侧边栏壁纸
博主头像
可可同学博主等级

欲买桂花同载酒

  • 累计撰写 7 篇文章
  • 累计创建 7 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

【新生指南-特别篇】Sunpanel美化汇总教程(持续更新)

可可爸爸
2024-12-27 / 1 评论 / 7 点赞 / 1271 阅读 / 66737 字 / 正在检测是否收录...

🎄前言

近来没事儿就折腾折腾Sun-Panel,发现大家对这个美化还是挺有兴趣的,出一个汇总教程,有兴趣的朋友们可以参考下,有好玩的我还会更新帖子。

效果预览:https://home.cocoyoo.cn/

部分原版方案来自网络,感谢原创作者:Sun-Panel YM-NAV 自定义css js 源码思路IT 人必备工具箱、时也命也

🎁前置条件

🎫本文所需附件:飞牛私有云分享:https://s.fnnas.net/s/181f744e69ba4686a2,密码:madrays,打开链接可下载文件

🎆为了支持作者@红烧猎人,本教程大多数需开通订阅后通过方能实现:

🎰多JS、CSS调用脚本

// 加载脚本函数
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));
    try {
        await Promise.all(stylePromises);
    } catch (error) {
        console.error(error);
    }
}

// 调用函数加载多个样式表
const scriptsToLoad = [
    '/custom/fishbackground.js',
    '/custom/ai.js',
    '/custom/toc.js',
    '/custom/mouse.js'
    // '/custom/待添加.js', // 如果需要,可以解除注释
];
// 调用函数加载多个脚本
const stylesToLoad = [
    '/custom/bk.css',
    '/custom/action.css',
    '/custom/logo.css',
    '/custom/xiantiao.css',
    '/custom/mouse.css',
    '/custom/loading.css'
    // '/custom/待添加.css', // 如果需要,可以解除注释
];

// 调用加载函数
loadScripts(scriptsToLoad);
loadStyles(stylesToLoad);

♦️上述代码为附件all.js文件,记事本或VSCode等软件打开后复制到上述JS脚本输入框即可,需要什么美化,就自己修改下调用路径,这个应该看得懂吧。

♥️美化案例

✨1.渐变背景

body {
  /* 100%窗口高度 */
  height: 100vh;

  /* 更深的色调和不同的渐变方向 */
  background: linear-gradient(45deg, #2C3E50, #2980B9, #8E44AD, #E74C3C);

  /* 指定背景图像的大小 */
  background-size: 400% 400%;

  /* 执行动画:动画名 时长 缓动函数 无限次播放 */
  animation: action 30s ease-in-out infinite;
}

/* 定义动画 */
@keyframes action {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

上述代码为附件中的bk.css文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个样式表下方增加一行'/custom/bk.css',即可调用。需注意渐变背景需删除原有背景才可以实现:壁纸-编辑图像链接-清空输入框-保存

✨2.鼠标悬停动画

/*鼠标悬停动画 */

/* 当鼠标悬停在图标信息框上时触发动画 */
/* 详细图标摇晃动画 */
.icon-info-box .rounded-2xl:hover {
  background: #b3f1e2e6 !important;
  /* 背景颜色变成深灰色 */
  -webkit-animation: info-shake-bounce .5s alternate !important;
  -moz-animation: info-shake-bounce .5s alternate !important;
  -o-animation: info-shake-bounce .5s alternate !important;
  animation: info-shake-bounce .5s alternate !important;
}

/* 小图标摇晃动画 */
.icon-small-box .rounded-2xl:hover {
  background: #b3f1e2e6 !important;
  /* 背景颜色变成深灰色 */
  -webkit-animation: small-shake-bounce .5s alternate !important;
  -moz-animation: small-shake-bounce .5s alternate !important;
  -o-animation: small-shake-bounce .5s alternate !important;
  animation: small-shake-bounce .5s alternate !important;
}

/* 定义摇详细图标晃弹跳动画的关键帧 */
@keyframes info-shake-bounce {

  0%,
  100% {
    transform: rotate(0);
  }

  25% {
    transform: rotate(10deg);
  }

  50% {
    transform: rotate(-10deg);
  }

  75% {
    transform: rotate(2.5deg);
  }

  85% {
    transform: rotate(-2.5deg);
  }
}

/* 定义摇小图标晃弹跳动画的关键帧 */
@keyframes small-shake-bounce {

  0%,
  100% {
    transform: rotate(0);
  }

  25% {
    transform: rotate(15deg);
  }

  50% {
    transform: rotate(-15deg);
  }

  75% {
    transform: rotate(5deg);
  }

  85% {
    transform: rotate(5deg);
  }
}

上述代码为附件中的action.css文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个样式表下方增加一行'/custom/action.css',即可调用。

✨3.网页播放器

网页播放器尝试了各种开源的,还是只有明月浩空网的能用,点这里注册账号,按教程自定义下,免费的也够用了,需要注意我们设置播放器放置在右侧,为sunpanel左侧边栏留出位置避免冲突,然后要开启加载jQuery

试了半天,免插件代码用不了哈!

只能用JS代码了,替换下面代码中的yourid为你的播放器ID即可。

var script = document.createElement("script");
script.setAttribute("type","text/javascript");
script.setAttribute("id","myhk");
script.setAttribute("src","https://myhkw.cn/api/player/yourid");
script.setAttribute("key","yourid");
document.documentElement.appendChild(script);

上述代码为附件中的myhk.js文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个脚本下方增加一行'/custom/myhk.js',即可调用。

✨4.白化版侧边导航栏

(function () {
    // =========== Config Start ===========
    // ------------------------------------
    // 距离滚动偏移量
    const scrollOffset = 80
  
    // 显示风格( auto:自动(默认) | mobile:左上角显示触发按钮-移动端风格 | sidebar:常态显示侧栏)
    const displayStyle = 'auto'
  
    // 移动端宽度定义
    const mobileWidth = 800
  
    const SunPanelTOCDomIdName = 'sun-panel-toc-dom'
  
    // 左上角按钮 SVG 图标
    const svgTocMobileBtn = '<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 24 24"><path fill="currentColor" d="M17.5 4.5c-1.95 0-4.05.4-5.5 1.5c-1.45-1.1-3.55-1.5-5.5-1.5c-1.45 0-2.99.22-4.28.79C1.49 5.62 1 6.33 1 7.14v11.28c0 1.3 1.22 2.26 2.48 1.94c.98-.25 2.02-.36 3.02-.36c1.56 0 3.22.26 4.56.92c.6.3 1.28.3 1.87 0c1.34-.67 3-.92 4.56-.92c1 0 2.04.11 3.02.36c1.26.33 2.48-.63 2.48-1.94V7.14c0-.81-.49-1.52-1.22-1.85c-1.28-.57-2.82-.79-4.27-.79M21 17.23c0 .63-.58 1.09-1.2.98c-.75-.14-1.53-.2-2.3-.2c-1.7 0-4.15.65-5.5 1.5V8c1.35-.85 3.8-1.5 5.5-1.5c.92 0 1.83.09 2.7.28c.46.1.8.51.8.98z"/><path fill="currentColor" d="M13.98 11.01c-.32 0-.61-.2-.71-.52c-.13-.39.09-.82.48-.94c1.54-.5 3.53-.66 5.36-.45c.41.05.71.42.66.83s-.42.71-.83.66c-1.62-.19-3.39-.04-4.73.39c-.08.01-.16.03-.23.03m0 2.66c-.32 0-.61-.2-.71-.52c-.13-.39.09-.82.48-.94c1.53-.5 3.53-.66 5.36-.45c.41.05.71.42.66.83s-.42.71-.83.66c-1.62-.19-3.39-.04-4.73.39a1 1 0 0 1-.23.03m0 2.66c-.32 0-.61-.2-.71-.52c-.13-.39.09-.82.48-.94c1.53-.5 3.53-.66 5.36-.45c.41.05.71.42.66.83s-.42.7-.83.66c-1.62-.19-3.39-.04-4.73.39a1 1 0 0 1-.23.03"/></svg>'
  
    // ------------------------------------
    // =========== Config End ===========
  
    // 滚动容器的类名
    const scrollContainerElementClassName = '.scroll-container'
  
    // 一些函数
    const isMobile = () => {
      if (displayStyle === 'mobile') {
        return true
      }
      else if (displayStyle === 'pc') {
        return false
      }
      const width = window.innerWidth
      return width < mobileWidth
    }
  
    function createDom() {
      // 检测是否已经存在TOC DOM,存在则删除
      (function () {
        const element = document.getElementById(SunPanelTOCDomIdName)
        if (element) {
          element.remove()
        }
      })()
  
      const SunPanelTOCDom = document.createElement('div')
      SunPanelTOCDom.id = SunPanelTOCDomIdName
      document.body.appendChild(SunPanelTOCDom)
  
      // ========= Add style start =========
      const style = document.createElement('style')
      const SunPanelTOCDomStyleId = `#${SunPanelTOCDomIdName}`
      style.textContent = `
      ${SunPanelTOCDomStyleId} #toc-mobile-btn {
          top: 20px !important;
          left: 20px !important;
          position: fixed;
          width: 46px;
          height: 46px;
          background-color:#2a2a2a6b;
          color: white;
          border-radius: 0.5rem;
          display: flex;
          justify-content: center;
          align-items: center;
          cursor: pointer;
      }
  
      ${SunPanelTOCDomStyleId} .hidden {
          display: none !important;
      }
  
      ${SunPanelTOCDomStyleId} #toc-sidebar {
          width: 40px;
          padding: 10px;
          position: fixed;
          top: 0;
          left: 0;
          height: 100%;
          overflow: hidden;
          display: flex;
          flex-direction: column;
          justify-content: center;
          transition: width 0.3s ease, background-color 0.3s ease;
          border-top-right-radius: 20px;
          border-bottom-right-radius: 20px;
          background-color: none;
      }
  
      ${SunPanelTOCDomStyleId} .toc-mobile-btn-svg-container{
        width:21px;
        height:21px;
      }
  
      ${SunPanelTOCDomStyleId} .toc-sidebar-expansion {
          width: 200px !important;
          display: flex;
          background-color: rgb(255 255 255 / 20%);
          box-shadow: 1px 0 5px rgba(247, 238, 238, 0.66);
      }
  
      ${SunPanelTOCDomStyleId} #toc-sidebar .toc-sidebar-box {
          width: 500px;
      }
  
      ${SunPanelTOCDomStyleId} .title-bar-box {
          display: flex;
          align-items: center;
          position: relative;
          cursor: pointer;
      }
  
      ${SunPanelTOCDomStyleId} .title-bar-slip {
          width: 20px;
          height: 6px;
          background-color: white;
          border-radius: 5px;
          margin: 15px 0;
          transition: height 0.3s ease, width 0.3s ease;
          box-shadow: 1px 0 5px rgba(248, 216, 216, 0.88);
      }
  
      ${SunPanelTOCDomStyleId} .title-bar-title {
          opacity: 0;
          white-space: nowrap;
          transition: opacity 0.3s ease, transform 0.3s ease, margin-left 0.3s ease;
          font-size: 15px;
          color:rgba(255, 255, 255, 1);
      }
  
      ${SunPanelTOCDomStyleId} .toc-sidebar-expansion .title-bar-title {
          opacity: 1;
          margin-left: 10px;
      }
  
      ${SunPanelTOCDomStyleId} .toc-sidebar-expansion .title-bar-slip {
          box-shadow: none;
      }
  
      ${SunPanelTOCDomStyleId} .toc-sidebar-expansion .title-bar-box:hover .title-bar-slip {
          width: 40px;
      }
  
      ${SunPanelTOCDomStyleId} .toc-sidebar-expansion .title-bar-box:hover .title-bar-title {
          font-size: 20px;
      }
  
        `
      // 添加样式到文档头部
      SunPanelTOCDom.appendChild(style)
  
      // ========= Add style end =========
  
      // 添加移动端菜单按钮
      const tocMobileBtn = document.createElement('div')
      tocMobileBtn.id = 'toc-mobile-btn'
      tocMobileBtn.classList.add('backdrop-blur-[2px]')
      SunPanelTOCDom.appendChild(tocMobileBtn)
  
      const tocMobileBtnSvgcContainer = document.createElement('div')
      tocMobileBtnSvgcContainer.innerHTML = svgTocMobileBtn
      tocMobileBtnSvgcContainer.classList.add('toc-mobile-btn-svg-container')
      tocMobileBtn.appendChild(tocMobileBtnSvgcContainer)
  
      // 创建侧边栏容器
      const sidebar = document.createElement('div')
      sidebar.id = 'toc-sidebar'
  
      const sidebarBox = document.createElement('div')
      sidebarBox.className = 'toc-sidebar-box'
  
      // 查询出所有类名包含 item-group-index- 的元素
      const items = document.querySelectorAll('[class*="item-group-index-"]')
  
      // 遍历并打印每个元素的完整类名
      items.forEach((item) => {
        item.classList.forEach((className) => {
          if (className.startsWith('item-group-index-')) {
            const titleBarBox = document.createElement('div')
            titleBarBox.className = 'title-bar-box'
            // titleBarBox.href = `#${item.id}`
            titleBarBox.dataset.groupClassName = className
  
            // 目录条
            const titleBarSlip = document.createElement('div')
            titleBarSlip.className = 'title-bar-slip'
  
            // 创建一个链接
            const titleBarTitle = document.createElement('div')
            titleBarTitle.className = 'title-bar-title'
  
            // 获取子元素中 class="group-title" 的内容
            const titleElement = item.querySelector('.group-title')
            const titleText = titleElement ? titleElement.textContent : item.id
            titleBarTitle.textContent = titleText
  
            titleBarBox.appendChild(titleBarSlip)
            titleBarBox.appendChild(titleBarTitle)
  
            sidebarBox.appendChild(titleBarBox)
          }
        })
      })
  
      sidebar.appendChild(sidebarBox)
  
      // 将侧边栏添加到页面中
      SunPanelTOCDom.appendChild(sidebar)
  
      function mobileHideSidebar() {
        sidebar.classList.remove('toc-sidebar-expansion')
        sidebar.classList.add('hidden')
      }
  
      function hideSidebar() {
        sidebar.classList.remove('toc-sidebar-expansion')
      }
  
      function showSidebar() {
        sidebar.classList.add('toc-sidebar-expansion')
        sidebar.classList.remove('hidden')
      }
  
      // ----------------
      // 监听宽度变化开始
      // ----------------
      function debounce(func, wait) {
        let timeout
        return function (...args) {
          clearTimeout(timeout)
          timeout = setTimeout(() => {
            func.apply(this, args)
          }, wait)
        }
      }
  
      function handleResize() {
        if (isMobile()) {
          tocMobileBtn.classList.remove('hidden')
          sidebar.classList.add('hidden')
        }
        else {
          tocMobileBtn.classList.add('hidden')
          sidebar.classList.remove('hidden')
        }
      }
  
      // 使用防抖函数包装你的处理函数
      const debouncedHandleResize = debounce(handleResize, 200)
  
      // 添加事件监听器
      window.addEventListener('resize', debouncedHandleResize)
  
      // 首次触发
      handleResize()
  
      // ----------------
      // 监听宽度变化结束
      // ----------------
  
      // 监听移动端按钮点击
      tocMobileBtn.addEventListener('click', () => {
        if (sidebar.classList.contains('toc-sidebar-expansion')) {
          // 隐藏
          mobileHideSidebar()
        }
        else {
          // 显示
          showSidebar()
        }
      })
  
      // 监听TOC栏失去hover
      sidebar.addEventListener('mouseleave', () => {
        if (isMobile()) {
          // 隐藏
          mobileHideSidebar()
        }
        else {
          hideSidebar()
        }
      })
  
      // 监听TOC栏获得hover
      sidebar.addEventListener('mouseenter', () => {
        showSidebar()
      })
  
      // 监听TOC点击事件
      document.querySelectorAll('.title-bar-box').forEach((box) => {
        box.addEventListener('click', function (event) {
        // 检查触发事件的元素是否有 'data-groupClassName' 属性
          if (this.dataset.groupClassName) {
          // 获取 'data-groupClass' 属性的值
            const groupClassName = this.dataset.groupClassName
            // 使用属性值作为选择器查询对应的元素
            const targetElement = document.querySelector(`.${groupClassName}`)
            if (targetElement) {
            // 获取目标元素的 'top' 坐标
              const targetTop = targetElement.offsetTop
              const scrollContainerElement = document.querySelector(scrollContainerElementClassName)
              if (scrollContainerElement) {
                scrollContainerElement.scrollTo({
                  top: targetTop - scrollOffset,
                  behavior: 'smooth', // 平滑滚动
                })
              }
            }
          }
        })
      })
    }
  
    // 判断是否已经存在分组,不存在将定时监听
    const items = document.querySelectorAll('[class*="item-group-index-"]')
    if (items.length > 0) {
      createDom()
      return
    }
  
    const interval = setInterval(() => {
      const items = document.querySelectorAll('[class*="item-group-index-"]')
      if (items.length > 0) {
        createDom()
        clearInterval(interval)
      }
    }, 1000)
  })()

上述代码为附件中的toc.js文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个脚本下方增加一行'/custom/toc.js',即可调用。

✨5.logo改自定义图片

.logo span {
  display: none; /* 隐藏原有的文字 */
}

.logo {
  background-image: url('/custom/logo.png'); /* 设置背景图片,上传此图片至./conf/custom/文件夹下 */
  background-size: contain; /* 确保图片完整显示 */
  background-repeat: no-repeat; /* 防止图片重复 */
  background-position: center; /* 图片居中显示 */
  width: 40%; /* 或者设置为具体的宽度 */
  height: 120px; /* 或者设置为具体的高度,根据需要调整 */
}

上述代码为附件中的logo.css文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个样式表下方增加一行'/custom/logo.css',即可调用。

✨6.图标线条背景


/* 背景线条样式 BY 香水 [二群大佬提供] */

/* 伪元素创建背景线条样式 */
.w-full .font-semibold:before {
  position: absolute;  /* 设置为绝对定位 */
  width: 93px;  /* 宽度为93像素 */
  display: block;  /* 设置为块级元素 */
  height: 75px;  /* 高度为75像素 */
  content: "";  /* 伪元素内容为空 */
  border-radius: 60%;  /* 边框半径为50%,形成圆形 */
  z-index: -1;  /* 设置层级为-1,将其放在内容之后 */
  right: -27px;  /* 距离右边-27像素的位置 */
  top: -35px;  /* 距离顶部-35像素的位置 */
  background: #efcece2f;  /* 背景颜色为淡白色带透明度的3b */
  box-shadow: -8px 21px 0 #ceefe132;  /* 设置阴影效果,水平偏移-8px,垂直偏移21px,颜色为淡白色带透明度的1a */
}

/* 伪元素创建另一种背景线条样式 */
.w-full .font-semibold:after {
  position: absolute;  /* 设置为绝对定位 */
  width: 40px;  /* 宽度为40像素 */
  display: block;  /* 设置为块级元素 */
  height: 40px;  /* 高度为40像素 */
  border: 4px solid #ebece342;  /* 边框为4像素的实线,颜色为淡白色带透明度的3b */
  content: "";  /* 伪元素内容为空 */
  border-radius: 70%;  /* 边框半径为50%,形成圆形 */
  top: -19px;  /* 距离顶部-19像素的位置 */
  right: 48px;  /* 距离右边48像素的位置 */
  z-index: -1;  /* 设置层级为-1,将其放在内容之后 */
}

/* 设置图标信息框的圆角样式 */
.icon-info-box .rounded-2xl {
  position: relative;  /* 设置为相对定位 */
  border-radius: 15px;  /* 设置边框半径为15像素,形成圆角 */
  overflow: hidden;  /* 超出部分隐藏 */
  -webkit-backdrop-filter: blur(10px);  /* 使用Webkit前缀的背景滤镜,模糊程度为10像素 */
  backdrop-filter: blur(10px);  /* 背景滤镜,模糊程度为10像素 */
}

上述代码为附件中的xiantiao.css文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个样式表下方增加一行'/custom/xiantiao.css',即可调用。

✨7.AI小助手

loadScript('https://test.com:8888/api/application/embed?protocol=https&host=test.com:8888&token=yourtoken');

上述代码来自自行部署的MaxKB,部署使用本教程不赘述,创建好的应用中有嵌入第三方代码,取下图中的网址替换上述代码中引号内的内容。

上述代码为附件中的ai.js文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个脚本下方增加一行'/custom/ai.js',即可调用。

✨8.全局字体替换

/* 自定义字体 */
@font-face {
  font-family: "Font";
  src: url("/custom/字体.ttf"); /* 自行下载字体存放至sunpanel安装目录的./conf/custom/文件夹下 */
}

/* 自定义全局字体 */
* {
  font-family: Font;
}

上述代码为附件中的font.css文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个样式表下方增加一行'/custom/font.css',即可调用。

✨9.飞鱼动态页脚


var RENDERER = {
    POINT_INTERVAL : 5,
    FISH_COUNT : 3,
    MAX_INTERVAL_COUNT : 50,
    INIT_HEIGHT_RATE : 0.5,
    THRESHOLD : 50,
    
    init : function(){
        this.setParameters();
        this.reconstructMethods();
        this.setup();
        this.bindEvent();
        this.render();
        console.log('debug');
    },
    setParameters : function(){
        this.$window = window;
        this.$document = document.body
        this.$container = document.getElementById('jsi-flying-fish-container');
        this.$canvas = document.createElement('canvas');
        this.$container.appendChild(this.$canvas)
        this.context = this.$canvas.getContext('2d');
        this.points = [];
        this.fishes = [];
        this.watchIds = [];
    },
    createSurfacePoints : function(){
        var count = Math.round(this.width / this.POINT_INTERVAL);
        this.pointInterval = this.width / (count - 1);
        this.points.push(new SURFACE_POINT(this, 0));
        
        for(var i = 1; i < count; i++){
            var point = new SURFACE_POINT(this, i * this.pointInterval),
                previous = this.points[i - 1];
                
            point.setPreviousPoint(previous);
            previous.setNextPoint(point);
            this.points.push(point);
        }
    },
    reconstructMethods : function(){
        this.watchWindowSize = this.watchWindowSize.bind(this);
        this.jdugeToStopResize = this.jdugeToStopResize.bind(this);
        this.startEpicenter = this.startEpicenter.bind(this);
        this.moveEpicenter = this.moveEpicenter.bind(this);
        this.reverseVertical = this.reverseVertical.bind(this);
        this.render = this.render.bind(this);
    },
    setup : function(){
        this.points.length = 0;
        this.fishes.length = 0;
        this.watchIds.length = 0;
        this.intervalCount = this.MAX_INTERVAL_COUNT;
        this.width = this.$container.offsetWidth;
        this.height = this.$container.offsetHeight;
        this.fishCount = this.FISH_COUNT * this.width / 500 * this.height / 500;
        this.$canvas.width = this.width;
        this.$canvas.height = this.height;
        this.reverse = false;
        
        this.fishes.push(new FISH(this));
        this.createSurfacePoints();
    },
    watchWindowSize : function(){
        this.clearTimer();
        this.tmpWidth = this.$window.width;
        this.tmpHeight = this.$window.height;
        this.watchIds.push(setTimeout(this.jdugeToStopResize, this.WATCH_INTERVAL));
    },
    clearTimer : function(){
        while(this.watchIds.length > 0){
            clearTimeout(this.watchIds.pop());
        }
    },
    jdugeToStopResize : function(){
        var width = this.$window.width(),
            height = this.$window.height(),
            stopped = (width == this.tmpWidth && height == this.tmpHeight);
            
        this.tmpWidth = width;
        this.tmpHeight = height;
        
        if(stopped){
            this.setup();
        }
    },
    bindEvent : function(){
        
        this.$window.onresize = this.watchWindowSize;
        this.$container.onclick = this.reverseVertical;
        this.$container.onmouseenter = this.startEpicenter;
        this.$container.addEventListener('onmousemove', this.moveEpicenter);
        
    },
    getAxis : function(event){
 
        var offset = this.getOffset(this.$container);
        return {
            x : event.clientX - offset.left + this.$document.scrollLeft,
            y : event.clientY - offset.top + this.$document.scrollTop
        };
    },
 
    getOffset: function(Node, offset) {    
        if (!offset) {        
              offset = {};
              offset.top = 0; 
              offset.left = 0;
        }
        if (Node == document.body) {
                //当该节点为body节点时,结束递归        
                return offset;   
         }
        offset.top += Node.offsetTop;    offset.left += Node.offsetLeft;
        return this.getOffset(Node.parentNode, offset);//向上累加offset里的值
    },
    startEpicenter : function(event){
        this.axis = this.getAxis(event);
    },
    moveEpicenter : function(event){
        var axis = this.getAxis(event);
        
        if(!this.axis){
            this.axis = axis;
        }
        this.generateEpicenter(axis.x, axis.y, axis.y - this.axis.y);
        this.axis = axis;
    },
    generateEpicenter : function(x, y, velocity){
        if(y < this.height / 2 - this.THRESHOLD || y > this.height / 2 + this.THRESHOLD){
            return;
        }
        var index = Math.round(x / this.pointInterval);
        
        if(index < 0 || index >= this.points.length){
            return;
        }
        this.points[index].interfere(y, velocity);
    },
    reverseVertical : function(){
        this.reverse = !this.reverse;
        
        for(var i = 0, count = this.fishes.length; i < count; i++){
            this.fishes[i].reverseVertical();
        }
    },
    controlStatus : function(){
        for(var i = 0, count = this.points.length; i < count; i++){
            this.points[i].updateSelf();
        }
        for(var i = 0, count = this.points.length; i < count; i++){
            this.points[i].updateNeighbors();
        }
        if(this.fishes.length < this.fishCount){
            if(--this.intervalCount == 0){
                this.intervalCount = this.MAX_INTERVAL_COUNT;
                this.fishes.push(new FISH(this));
            }
        }
    },
    render : function(){
        requestAnimationFrame(this.render);
        this.controlStatus();
        this.context.clearRect(0, 0, this.width, this.height);
        this.context.fillStyle = 'hsl(0, 0%, 95%)';
        
        for(var i = 0, count = this.fishes.length; i < count; i++){
            this.fishes[i].render(this.context);
        }
        this.context.save();
        this.context.globalCompositeOperation = 'xor';
        this.context.beginPath();
        this.context.moveTo(0, this.reverse ? 0 : this.height);
        
        for(var i = 0, count = this.points.length; i < count; i++){
            this.points[i].render(this.context);
        }
        this.context.lineTo(this.width, this.reverse ? 0 : this.height);
        this.context.closePath();
        this.context.fill();
        this.context.restore();
    }
};
var SURFACE_POINT = function(renderer, x){
    this.renderer = renderer;
    this.x = x;
    this.init();
};
SURFACE_POINT.prototype = {
    SPRING_CONSTANT : 0.03,
    SPRING_FRICTION : 0.9,
    WAVE_SPREAD : 0.3,
    ACCELARATION_RATE : 0.01,
    
    init : function(){
        this.initHeight = this.renderer.height * this.renderer.INIT_HEIGHT_RATE;
        this.height = this.initHeight;
        this.fy = 0;
        this.force = {previous : 0, next : 0};
    },
    setPreviousPoint : function(previous){
        this.previous = previous;
    },
    setNextPoint : function(next){
        this.next = next;
    },
    interfere : function(y, velocity){
        this.fy = this.renderer.height * this.ACCELARATION_RATE * ((this.renderer.height - this.height - y) >= 0 ? -1 : 1) * Math.abs(velocity);
    },
    updateSelf : function(){
        this.fy += this.SPRING_CONSTANT * (this.initHeight - this.height);
        this.fy *= this.SPRING_FRICTION;
        this.height += this.fy;
    },
    updateNeighbors : function(){
        if(this.previous){
            this.force.previous = this.WAVE_SPREAD * (this.height - this.previous.height);
        }
        if(this.next){
            this.force.next = this.WAVE_SPREAD * (this.height - this.next.height);
        }
    },
    render : function(context){
        if(this.previous){
            this.previous.height += this.force.previous;
            this.previous.fy += this.force.previous;
        }
        if(this.next){
            this.next.height += this.force.next;
            this.next.fy += this.force.next;
        }
        context.lineTo(this.x, this.renderer.height - this.height);
    }
};
var FISH = function(renderer){
    this.renderer = renderer;
    this.init();
};
FISH.prototype = {
    GRAVITY : 0.4,
    
    init : function(){
        this.direction = Math.random() < 0.5;
        this.x = this.direction ? (this.renderer.width + this.renderer.THRESHOLD) : -this.renderer.THRESHOLD;
        this.previousY = this.y;
        this.vx = this.getRandomValue(4, 10) * (this.direction ? -1 : 1);
        
        if(this.renderer.reverse){
            this.y = this.getRandomValue(this.renderer.height * 1 / 10, this.renderer.height * 4 / 10);
            this.vy = this.getRandomValue(2, 5);
            this.ay = this.getRandomValue(0.05, 0.2);
        }else{
            this.y = this.getRandomValue(this.renderer.height * 6 / 10, this.renderer.height * 9 / 10);
            this.vy = this.getRandomValue(-5, -2);
            this.ay = this.getRandomValue(-0.2, -0.05);
        }
        this.isOut = false;
        this.theta = 0;
        this.phi = 0;
    },
    getRandomValue : function(min, max){
        return min + (max - min) * Math.random();
    },
    reverseVertical : function(){
        this.isOut = !this.isOut;
        this.ay *= -1;
    },
    controlStatus : function(context){
        this.previousY = this.y;
        this.x += this.vx;
        this.y += this.vy;
        this.vy += this.ay;
        
        if(this.renderer.reverse){
            if(this.y > this.renderer.height * this.renderer.INIT_HEIGHT_RATE){
                this.vy -= this.GRAVITY;
                this.isOut = true;
            }else{
                if(this.isOut){
                    this.ay = this.getRandomValue(0.05, 0.2);
                }
                this.isOut = false;
            }
        }else{
            if(this.y < this.renderer.height * this.renderer.INIT_HEIGHT_RATE){
                this.vy += this.GRAVITY;
                this.isOut = true;
            }else{
                if(this.isOut){
                    this.ay = this.getRandomValue(-0.2, -0.05);
                }
                this.isOut = false;
            }
        }
        if(!this.isOut){
            this.theta += Math.PI / 20;
            this.theta %= Math.PI * 2;
            this.phi += Math.PI / 30;
            this.phi %= Math.PI * 2;
        }
        this.renderer.generateEpicenter(this.x + (this.direction ? -1 : 1) * this.renderer.THRESHOLD, this.y, this.y - this.previousY);
        
        if(this.vx > 0 && this.x > this.renderer.width + this.renderer.THRESHOLD || this.vx < 0 && this.x < -this.renderer.THRESHOLD){
            this.init();
        }
    },
    render : function(context){
        context.save();
        context.translate(this.x, this.y);
        context.rotate(Math.PI + Math.atan2(this.vy, this.vx));
        context.scale(1, this.direction ? 1 : -1);
        context.beginPath();
        context.moveTo(-30, 0);
        context.bezierCurveTo(-20, 15, 15, 10, 40, 0);
        context.bezierCurveTo(15, -10, -20, -15, -30, 0);
        context.fill();
        
        context.save();
        context.translate(40, 0);
        context.scale(0.9 + 0.2 * Math.sin(this.theta), 1);
        context.beginPath();
        context.moveTo(0, 0);
        context.quadraticCurveTo(5, 10, 20, 8);
        context.quadraticCurveTo(12, 5, 10, 0);
        context.quadraticCurveTo(12, -5, 20, -8);
        context.quadraticCurveTo(5, -10, 0, 0);
        context.fill();
        context.restore();
        
        context.save();
        context.translate(-3, 0);
        context.rotate((Math.PI / 3 + Math.PI / 10 * Math.sin(this.phi)) * (this.renderer.reverse ? -1 : 1));
        
        context.beginPath();
        
        if(this.renderer.reverse){
            context.moveTo(5, 0);
            context.bezierCurveTo(10, 10, 10, 30, 0, 40);
            context.bezierCurveTo(-12, 25, -8, 10, 0, 0);
        }else{
            context.moveTo(-5, 0);
            context.bezierCurveTo(-10, -10, -10, -30, 0, -40);
            context.bezierCurveTo(12, -25, 8, -10, 0, 0);
        }
        context.closePath();
        context.fill();
        context.restore();
        context.restore();
        this.controlStatus(context);
    }
};

document.addEventListener('DOMContentLoaded', (event) => { 
    
    // 用于添加视频背景的函数  
    const addFishBackground = (wallpaperDiv) => {  
      
         // 创建一个新的div元素 
      var newDiv = document.createElement("div");        
      // 设置div的id属性 
      newDiv.setAttribute("id",  "jsi-flying-fish-container");        
      // 设置div的class属性 
      newDiv.setAttribute("class",  "fishcontainer");        
      // 设置div的样式 
      newDiv.style.width  = "100%"; 
      newDiv.style.height  = "200px"; 
      newDiv.style.position  = "fixed"; 
      newDiv.style.zIndex  = "0"; 
      newDiv.style.opacity  = "0.37"; 
      newDiv.style.bottom  = "0"; 
      newDiv.style.left  = "0"; 
       
      // 将新创建的div元素添加到body中 
      wallpaperDiv.appendChild(newDiv);  
    };  
  
    // 使用MutationObserver监视DOM变化  
    const observer = new MutationObserver((mutationsList, observer) => {  
        // 查找匹配的.cover.wallpaper元素  
        const wallpaperDiv = document.querySelector('.cover.wallpaper');  
  
        if (wallpaperDiv && !wallpaperDiv.querySelector('.fishcontainer')) {  
            // 添加视频背景  
            addFishBackground(wallpaperDiv);  
            // 注意:我们不再断开观察者,以便它能够继续监视未来的变化
            RENDERER.init();
        }  
    });  
 
    // 启动观察者监视document.body的变化  
    observer.observe(document.body, { childList: true, subtree: true });  
});

上述代码为附件中的fishbackground.js文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个脚本下方增加一行'/custom/fishbackground.js',即可调用。

✨10.搜索栏下小组件(仅供参考,自行研究)

此方案需要配合自定义页脚来实现,下面是我在用的页脚,感兴趣的可以研究下,不是太完美。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        body, html {
            margin: 0;
            padding: 0;
        }
       .site-footer {
            padding: 30px 0;
            text-align: center;
            color: #fff;
        }
       .footer-content {
            display: flex;
            justify-content: center;
        }
       .footer-links {
            margin: 0;
            padding: 0;
            list-style: none;
        }
       .footer-links li {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }
       .footer-links a {
            text-decoration: none;
            color: inherit;
            transition: color 0.3s ease;
            display: flex;
            align-items: center;
        }
       .footer-links a:hover {
            color: #ccc;
        }
       .footer-icon {
            width: 35px;
            height: auto;
            margin-right: 15px;
        }
       .row {
            display: flex;
            flex-wrap: wrap; /* 允许换行 */
            justify-content: space-around; /* 平均分布 */
            width: 100%; /* 确保容器宽度 */
            margin-top: 20px; /* 在.row组件上方添加间距 */
        }
       .widget {
            flex: 1 1 100px; /* 允许组件扩展和收缩,最小宽度为300px */
            margin: 10px; /* 添加间距防止紧贴 */
            box-sizing: border-box; /* 确保padding和border包含在宽度内 */
        }
       .responsive-widget {
            width: 100%;
            margin-top: 20px; /* 在.responsive-widget组件上方添加间距 */
        }

        #ww_17bfa75f2cb5c_u, #ww_df9fcd9873fd2_u {
            display: none;
        }
    </style>

</head>
<body>
    <div class="spacer"></div> <!-- 上方空白空间 -->

    <div class="row">
        
        <!-- 异步加载天气组件脚本 -->
        <div id="ww_17bfa75f2cb5c" v='1.3' loc='id' a='{"t":"horizontal","lang":"zh","sl_lpl":1,"ids":["wl11510"],"font":"Arial","sl_ics":"one_a","sl_sot":"celsius","cl_bkg":"#FFFFFF00","cl_font":"rgba(255,255,255,1)","cl_cloud":"rgba(255,255,255,1)","cl_persp":"#2196F3","cl_sun":"#FFC107","cl_moon":"#FFC107","cl_thund":"#FF5722","el_nme":3}'>
            <a href="https://weatherwidget.org/" id="ww_17bfa75f2cb5c_u" target="_blank">Weather widget for website</a>
            <script async src="https://app3.weatherwidget.org/js/?id=ww_17bfa75f2cb5c"></script>
        </div>

        <!-- 使用loading="lazy"属性优化iframe加载 -->
        <iframe src="https://www.widgets.link/#/tools-hot-news?contentBoxShadowColor=FFFFFF0D&ac=F0B17F00&tc=19A7CE8C&ttc=ffffff&tic=ffffff&thc=ffffff&cc=FBE8D900&bg=&_b=true" class="widget" width="1500" height="210" loading="lazy"></iframe>
        <iframe src="https://www.widgets.link/#/typed?t=Hi,这里是Coco的导航页&bg=&tf=28&tt=ffffff&s=40&p=10&br=20&_b=true&pbg=FFFFFF00&bs=true&cf=16&cc=EEE8E8FF&c=随意享用吧~~" class="widget" width="33300" height="200" loading="lazy"></iframe>
        
        
        
        
        
    </div>

    <!-- 新增的天气组件,单独占据一行 -->
    <div class="responsive-widget">
        <div id="ww_df9fcd9873fd2" v='1.3' loc='id' a='{"t":"responsive","lang":"zh","sl_lpl":1,"ids":["wl11510"],"font":"Arial","sl_ics":"one_a","sl_sot":"celsius","cl_bkg":"#FFFFFF00","cl_font":"rgba(255,255,255,1)","cl_cloud":"rgba(255,255,255,1)","cl_persp":"#2196F3","cl_sun":"#FFC107","cl_moon":"#FFC107","cl_thund":"#FF5722","el_nme":3,"el_ctm":3,"el_cwi":3}'>
            <a href="https://weatherwidget.org/" id="ww_df9fcd9873fd2_u" target="_blank">Weather widget for website</a>
            <script async src="https://app3.weatherwidget.org/js/?id=ww_df9fcd9873fd2"></script>
        </div>
    </div>

    <footer class="site-footer">
        <div class="footer-content">
            <ul class="footer-links">
                <li></li>
                <li>
                    <a href="https://cocohe.cn" target="_blank" class="personal-link">
                        <img class="footer-icon" src="/uploads/2024/8/10/1985fc970e85cddfdb818d5d174fbde7.ico" alt="可可同学图标">
                        <span><font size="4" color=" #fff" style="font-family: 'STCaiyun'"><b>@ 可可同学</font></b></span>
                    </a>
                </li>
            </ul>
        </div>
    </footer>

    <script>
        // 获取目标元素
        var itemCardBox = document.getElementById("item-card-box");

        // 获取要插入的组件
        var weatherComponent1 = document.querySelector('.row');
        var weatherComponent = document.querySelector('.responsive-widget');

        // 检查目标元素是否存在
        if (itemCardBox) {
            // 在目标元素上方插入组件
            itemCardBox.insertAdjacentElement('beforebegin', weatherComponent1);
            itemCardBox.insertAdjacentElement('beforebegin', weatherComponent);
        } else {
            console.warn('目标元素未找到: item-card-box');
        }
    </script>
</body>
</html>

上述页脚中包含了两个天气组件,来自免费天气插件:https://weatherwidget.org/zh/,其中包含了自定义位置,大家需要去根据地址自己生成一下插件,对应修改下页脚中的代码内容,还包括了新闻组件和打字机组件,来自轻轻小组件:https://www.widgets.link/#/,可以自己挑选喜欢的组件修改对应页脚代码。

上述代码为附件中的页脚.html文件,记事本或VSCode打开后复制至自定义页脚输入框即可调用。

✨11.特殊:小鱼页脚和搜索栏下小组件完美共存方案

上面的方案中,如果同时使用小鱼页脚和搜索栏下小组件的话,小鱼页脚经常难以加载,最近终于找到了解决方案!主要是由于搜索栏下小组件影响了DOM事件,这样修改可以使小鱼页脚js脚本优先加载,后续遇到类似的情况应该也可以这样解决。

多JS、CSS调用脚本中直接加入小鱼页脚js代码,可放置在不影响加载效果的地方,我这里使用了动态背景,所以放在了动态背景下面,不然的话打开的时候会黑一下,大家根据自己的实际情况调整即可。

// 加载脚本函数
function loadScript(url) {
    const script = document.createElement('script');
    script.src = url;
    script.type = 'text/javascript';
    document.head.appendChild(script);
}

// 加载样式表函数
function loadStyle(url) {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = url;
    document.head.appendChild(link);
}



// 调用函数加载多个样式表
loadStyle('/custom/bk.css');
loadStyle('/custom/action.css');
loadStyle('/custom/logo.css');
loadStyle('/custom/xiantiao.css');
//loadStyle('/custom/待添加.css');


var RENDERER = {
    POINT_INTERVAL : 5,
    FISH_COUNT : 3,
    MAX_INTERVAL_COUNT : 50,
    INIT_HEIGHT_RATE : 0.5,
    THRESHOLD : 50,
    
    init : function(){
        this.setParameters();
        this.reconstructMethods();
        this.setup();
        this.bindEvent();
        this.render();
        console.log('debug');
    },
    setParameters : function(){
        this.$window = window;
        this.$document = document.body
        this.$container = document.getElementById('jsi-flying-fish-container');
        this.$canvas = document.createElement('canvas');
        this.$container.appendChild(this.$canvas)
        this.context = this.$canvas.getContext('2d');
        this.points = [];
        this.fishes = [];
        this.watchIds = [];
    },
    createSurfacePoints : function(){
        var count = Math.round(this.width / this.POINT_INTERVAL);
        this.pointInterval = this.width / (count - 1);
        this.points.push(new SURFACE_POINT(this, 0));
        
        for(var i = 1; i < count; i++){
            var point = new SURFACE_POINT(this, i * this.pointInterval),
                previous = this.points[i - 1];
                
            point.setPreviousPoint(previous);
            previous.setNextPoint(point);
            this.points.push(point);
        }
    },
    reconstructMethods : function(){
        this.watchWindowSize = this.watchWindowSize.bind(this);
        this.jdugeToStopResize = this.jdugeToStopResize.bind(this);
        this.startEpicenter = this.startEpicenter.bind(this);
        this.moveEpicenter = this.moveEpicenter.bind(this);
        this.reverseVertical = this.reverseVertical.bind(this);
        this.render = this.render.bind(this);
    },
    setup : function(){
        this.points.length = 0;
        this.fishes.length = 0;
        this.watchIds.length = 0;
        this.intervalCount = this.MAX_INTERVAL_COUNT;
        this.width = this.$container.offsetWidth;
        this.height = this.$container.offsetHeight;
        this.fishCount = this.FISH_COUNT * this.width / 500 * this.height / 500;
        this.$canvas.width = this.width;
        this.$canvas.height = this.height;
        this.reverse = false;
        
        this.fishes.push(new FISH(this));
        this.createSurfacePoints();
    },
    watchWindowSize : function(){
        this.clearTimer();
        this.tmpWidth = this.$window.width;
        this.tmpHeight = this.$window.height;
        this.watchIds.push(setTimeout(this.jdugeToStopResize, this.WATCH_INTERVAL));
    },
    clearTimer : function(){
        while(this.watchIds.length > 0){
            clearTimeout(this.watchIds.pop());
        }
    },
    jdugeToStopResize : function(){
        var width = this.$window.width(),
            height = this.$window.height(),
            stopped = (width == this.tmpWidth && height == this.tmpHeight);
            
        this.tmpWidth = width;
        this.tmpHeight = height;
        
        if(stopped){
            this.setup();
        }
    },
    bindEvent : function(){
        
        this.$window.onresize = this.watchWindowSize;
        this.$container.onclick = this.reverseVertical;
        this.$container.onmouseenter = this.startEpicenter;
        this.$container.addEventListener('onmousemove', this.moveEpicenter);
        
    },
    getAxis : function(event){
 
        var offset = this.getOffset(this.$container);
        return {
            x : event.clientX - offset.left + this.$document.scrollLeft,
            y : event.clientY - offset.top + this.$document.scrollTop
        };
    },
 
    getOffset: function(Node, offset) {    
        if (!offset) {        
              offset = {};
              offset.top = 0; 
              offset.left = 0;
        }
        if (Node == document.body) {
                //当该节点为body节点时,结束递归        
                return offset;   
         }
        offset.top += Node.offsetTop;    offset.left += Node.offsetLeft;
        return this.getOffset(Node.parentNode, offset);//向上累加offset里的值
    },
    startEpicenter : function(event){
        this.axis = this.getAxis(event);
    },
    moveEpicenter : function(event){
        var axis = this.getAxis(event);
        
        if(!this.axis){
            this.axis = axis;
        }
        this.generateEpicenter(axis.x, axis.y, axis.y - this.axis.y);
        this.axis = axis;
    },
    generateEpicenter : function(x, y, velocity){
        if(y < this.height / 2 - this.THRESHOLD || y > this.height / 2 + this.THRESHOLD){
            return;
        }
        var index = Math.round(x / this.pointInterval);
        
        if(index < 0 || index >= this.points.length){
            return;
        }
        this.points[index].interfere(y, velocity);
    },
    reverseVertical : function(){
        this.reverse = !this.reverse;
        
        for(var i = 0, count = this.fishes.length; i < count; i++){
            this.fishes[i].reverseVertical();
        }
    },
    controlStatus : function(){
        for(var i = 0, count = this.points.length; i < count; i++){
            this.points[i].updateSelf();
        }
        for(var i = 0, count = this.points.length; i < count; i++){
            this.points[i].updateNeighbors();
        }
        if(this.fishes.length < this.fishCount){
            if(--this.intervalCount == 0){
                this.intervalCount = this.MAX_INTERVAL_COUNT;
                this.fishes.push(new FISH(this));
            }
        }
    },
    render : function(){
        requestAnimationFrame(this.render);
        this.controlStatus();
        this.context.clearRect(0, 0, this.width, this.height);
        this.context.fillStyle = 'hsl(0, 0%, 95%)';
        
        for(var i = 0, count = this.fishes.length; i < count; i++){
            this.fishes[i].render(this.context);
        }
        this.context.save();
        this.context.globalCompositeOperation = 'xor';
        this.context.beginPath();
        this.context.moveTo(0, this.reverse ? 0 : this.height);
        
        for(var i = 0, count = this.points.length; i < count; i++){
            this.points[i].render(this.context);
        }
        this.context.lineTo(this.width, this.reverse ? 0 : this.height);
        this.context.closePath();
        this.context.fill();
        this.context.restore();
    }
};
var SURFACE_POINT = function(renderer, x){
    this.renderer = renderer;
    this.x = x;
    this.init();
};
SURFACE_POINT.prototype = {
    SPRING_CONSTANT : 0.03,
    SPRING_FRICTION : 0.9,
    WAVE_SPREAD : 0.3,
    ACCELARATION_RATE : 0.01,
    
    init : function(){
        this.initHeight = this.renderer.height * this.renderer.INIT_HEIGHT_RATE;
        this.height = this.initHeight;
        this.fy = 0;
        this.force = {previous : 0, next : 0};
    },
    setPreviousPoint : function(previous){
        this.previous = previous;
    },
    setNextPoint : function(next){
        this.next = next;
    },
    interfere : function(y, velocity){
        this.fy = this.renderer.height * this.ACCELARATION_RATE * ((this.renderer.height - this.height - y) >= 0 ? -1 : 1) * Math.abs(velocity);
    },
    updateSelf : function(){
        this.fy += this.SPRING_CONSTANT * (this.initHeight - this.height);
        this.fy *= this.SPRING_FRICTION;
        this.height += this.fy;
    },
    updateNeighbors : function(){
        if(this.previous){
            this.force.previous = this.WAVE_SPREAD * (this.height - this.previous.height);
        }
        if(this.next){
            this.force.next = this.WAVE_SPREAD * (this.height - this.next.height);
        }
    },
    render : function(context){
        if(this.previous){
            this.previous.height += this.force.previous;
            this.previous.fy += this.force.previous;
        }
        if(this.next){
            this.next.height += this.force.next;
            this.next.fy += this.force.next;
        }
        context.lineTo(this.x, this.renderer.height - this.height);
    }
};
var FISH = function(renderer){
    this.renderer = renderer;
    this.init();
};
FISH.prototype = {
    GRAVITY : 0.4,
    
    init : function(){
        this.direction = Math.random() < 0.5;
        this.x = this.direction ? (this.renderer.width + this.renderer.THRESHOLD) : -this.renderer.THRESHOLD;
        this.previousY = this.y;
        this.vx = this.getRandomValue(4, 10) * (this.direction ? -1 : 1);
        
        if(this.renderer.reverse){
            this.y = this.getRandomValue(this.renderer.height * 1 / 10, this.renderer.height * 4 / 10);
            this.vy = this.getRandomValue(2, 5);
            this.ay = this.getRandomValue(0.05, 0.2);
        }else{
            this.y = this.getRandomValue(this.renderer.height * 6 / 10, this.renderer.height * 9 / 10);
            this.vy = this.getRandomValue(-5, -2);
            this.ay = this.getRandomValue(-0.2, -0.05);
        }
        this.isOut = false;
        this.theta = 0;
        this.phi = 0;
    },
    getRandomValue : function(min, max){
        return min + (max - min) * Math.random();
    },
    reverseVertical : function(){
        this.isOut = !this.isOut;
        this.ay *= -1;
    },
    controlStatus : function(context){
        this.previousY = this.y;
        this.x += this.vx;
        this.y += this.vy;
        this.vy += this.ay;
        
        if(this.renderer.reverse){
            if(this.y > this.renderer.height * this.renderer.INIT_HEIGHT_RATE){
                this.vy -= this.GRAVITY;
                this.isOut = true;
            }else{
                if(this.isOut){
                    this.ay = this.getRandomValue(0.05, 0.2);
                }
                this.isOut = false;
            }
        }else{
            if(this.y < this.renderer.height * this.renderer.INIT_HEIGHT_RATE){
                this.vy += this.GRAVITY;
                this.isOut = true;
            }else{
                if(this.isOut){
                    this.ay = this.getRandomValue(-0.2, -0.05);
                }
                this.isOut = false;
            }
        }
        if(!this.isOut){
            this.theta += Math.PI / 20;
            this.theta %= Math.PI * 2;
            this.phi += Math.PI / 30;
            this.phi %= Math.PI * 2;
        }
        this.renderer.generateEpicenter(this.x + (this.direction ? -1 : 1) * this.renderer.THRESHOLD, this.y, this.y - this.previousY);
        
        if(this.vx > 0 && this.x > this.renderer.width + this.renderer.THRESHOLD || this.vx < 0 && this.x < -this.renderer.THRESHOLD){
            this.init();
        }
    },
    render : function(context){
        context.save();
        context.translate(this.x, this.y);
        context.rotate(Math.PI + Math.atan2(this.vy, this.vx));
        context.scale(1, this.direction ? 1 : -1);
        context.beginPath();
        context.moveTo(-30, 0);
        context.bezierCurveTo(-20, 15, 15, 10, 40, 0);
        context.bezierCurveTo(15, -10, -20, -15, -30, 0);
        context.fill();
        
        context.save();
        context.translate(40, 0);
        context.scale(0.9 + 0.2 * Math.sin(this.theta), 1);
        context.beginPath();
        context.moveTo(0, 0);
        context.quadraticCurveTo(5, 10, 20, 8);
        context.quadraticCurveTo(12, 5, 10, 0);
        context.quadraticCurveTo(12, -5, 20, -8);
        context.quadraticCurveTo(5, -10, 0, 0);
        context.fill();
        context.restore();
        
        context.save();
        context.translate(-3, 0);
        context.rotate((Math.PI / 3 + Math.PI / 10 * Math.sin(this.phi)) * (this.renderer.reverse ? -1 : 1));
        
        context.beginPath();
        
        if(this.renderer.reverse){
            context.moveTo(5, 0);
            context.bezierCurveTo(10, 10, 10, 30, 0, 40);
            context.bezierCurveTo(-12, 25, -8, 10, 0, 0);
        }else{
            context.moveTo(-5, 0);
            context.bezierCurveTo(-10, -10, -10, -30, 0, -40);
            context.bezierCurveTo(12, -25, 8, -10, 0, 0);
        }
        context.closePath();
        context.fill();
        context.restore();
        context.restore();
        this.controlStatus(context);
    }
};

document.addEventListener('DOMContentLoaded', (event) => { 
    
    // 用于添加视频背景的函数  
    const addFishBackground = (wallpaperDiv) => {  
      
         // 创建一个新的div元素 
      var newDiv = document.createElement("div");        
      // 设置div的id属性 
      newDiv.setAttribute("id",  "jsi-flying-fish-container");        
      // 设置div的class属性 
      newDiv.setAttribute("class",  "fishcontainer");        
      // 设置div的样式 
      newDiv.style.width  = "100%"; 
      newDiv.style.height  = "200px"; 
      newDiv.style.position  = "fixed"; 
      newDiv.style.zIndex  = "0"; 
      newDiv.style.opacity  = "0.37"; 
      newDiv.style.bottom  = "0"; 
      newDiv.style.left  = "0"; 
       
      // 将新创建的div元素添加到body中 
      wallpaperDiv.appendChild(newDiv);  
    };  
  
    // 使用MutationObserver监视DOM变化  
    const observer = new MutationObserver((mutationsList, observer) => {  
        // 查找匹配的.cover.wallpaper元素  
        const wallpaperDiv = document.querySelector('.cover.wallpaper');  
  
        if (wallpaperDiv && !wallpaperDiv.querySelector('.fishcontainer')) {  
            // 添加视频背景  
            addFishBackground(wallpaperDiv);  
            // 注意:我们不再断开观察者,以便它能够继续监视未来的变化
            RENDERER.init();
        }  
    });  
 
    // 启动观察者监视document.body的变化  
    observer.observe(document.body, { childList: true, subtree: true });  
});


// 调用函数加载多个脚本 
//loadScript('/custom/fishbackground.js');
loadScript('/custom/ai.js');
loadScript('/custom/toc.js');
//loadStyle('/custom/待添加.js');


♦️上述代码为附件all(小鱼页脚和小组件共存版).js文件,记事本或VSCode等软件打开后复制到上述JS脚本输入框即可,需要什么美化,就自己修改下调用路径(CSS在小鱼页脚代码的上面)。

✨12.搜索栏文字修改为自动更新一言

        // 定义一个函数,用于获取随机句子并更新占位符
        function updatePlaceholder() {
            fetch('https://v1.hitokoto.cn/')
                .then(response => response.json())
                .then(data => {
                    // 查找所有输入框
                    const inputElements = document.querySelectorAll('input[placeholder="请输入搜索内容"]');
                    if (inputElements.length > 0) {
                        // 遍历所有找到的输入框并更新占位符
                        inputElements.forEach(input => {
                            input.placeholder = data.hitokoto; 
                        });
                    }
                })
                .catch(error => {
                    console.error('获取句子时出错:', error);
                });
        }

        // 页面加载时自动调用替换函数
        window.onload = updatePlaceholder;

♦️上述代码为附件中的yiyan.js文件,存放至sunpanel安装目录的./conf/custom/文件夹下,在前置条件给出的js代码中// 调用函数加载多个脚本下方增加一行'/custom/ai.js',即可调用。

7
  • 1

评论区