收起左侧

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

23
回复
2942
查看
[ 复制链接 ]

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2024-12-27 15:34:50 显示全部楼层 阅读模式

[i=s] 本帖最后由 madrays 于 2025-1-4 17:51 编辑 [/i]<br /> <br />

🎄前言

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

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

我的blog原文:【新生指南-特别篇】Sunpanel美化汇总教程(持续更新)-可可同学

部分原版方案来自网络,感谢原创作者: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 in**lements = document.querySelectorAll('input[placeholder="请输入搜索内容"]');
                    if (in**lements.length > 0) {
                        // 遍历所有找到的输入框并更新占位符
                        in**lements.forEach(input => {
                            input.placeholder = data.hitokoto; 
                        });
                    }
                })
                .catch(error => {
                    console.error('获取句子时出错:', error);
                });
        }

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

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

收藏
送赞 5
分享

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2024-12-27 18:01:15 楼主 显示全部楼层
有需要自取

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2024-12-28 12:39:52 楼主 显示全部楼层
飞鱼页脚和搜索栏下方小组件有冲突,会导致飞鱼页脚有时加载不出来,目前没有好的解决方案

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2024-12-30 11:03:42 楼主 显示全部楼层
没人玩吗哈哈

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2024-12-31 17:21:57 楼主 显示全部楼层
....更新

0

主题

1

回帖

0

牛值

江湖小虾

2025-1-1 00:49:30 显示全部楼层

回帖奖励 +1 飞牛币

非常需要 感谢你的付出。 我正好在研究怎么搭建自己的sunpanel  幸好找到了你的资料

0

主题

2

回帖

0

牛值

江湖小虾

2025-1-3 15:31:46 显示全部楼层

回帖奖励 +1 飞牛币

很全面,我来试试。

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2025-1-4 17:53:39 楼主 显示全部楼层
今日更新,搜索栏占位符改动态一言0.0

1

主题

41

回帖

0

牛值

江湖小虾

2025-1-6 14:01:59 显示全部楼层

回帖奖励 +1 飞牛币

已经折腾好啦,非常感谢大佬!有一个小问题,yiyan的功能,偶尔会报错,不知道为什么
控制台信息如下:
Access to fetch at 'https://v1.hitokoto.cn/' from origin 'http://192.168.191.19:3002' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.了解此错误AI
yiyan.js:3
        
        
       GET https://v1.hitokoto.cn/ net::ERR_FAILED 403 (Forbidden)
updatePlaceholder @ yiyan.js:3
load
(匿名) @ yiyan.js:21了解此错误AI
yiyan.js:16 获取句子时出错: TypeError: Failed to fetch
    at updatePlaceholder (yiyan.js:3:13)
你用本地ip导致跨域了应该是  详情 回复
2025-1-6 15:30

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2025-1-6 15:30:40 楼主 显示全部楼层
你用本地ip导致跨域了应该是
是的,换了个api就好了,天气的那个小组件加载起来有点慢,有好的替代么?  详情 回复
2025-1-6 16:00

1

主题

41

回帖

0

牛值

江湖小虾

2025-1-6 16:00:13 显示全部楼层
madrays 发表于 2025-1-6 15:30
你用本地ip导致跨域了应该是

是的,换了个api就好了,天气的那个小组件加载起来有点慢,有好的替代么?
我自己做了 计划帮sunpanel做个美化的小项目 不着急的话可以等等哈  详情 回复
2025-1-6 16:13

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2025-1-6 16:13:23 楼主 显示全部楼层
我自己做了  计划帮sunpanel做个美化的小项目  不着急的话可以等等哈
太好了,期待!有github地址么,学习一下  详情 回复
2025-1-6 16:35

1

主题

41

回帖

0

牛值

江湖小虾

2025-1-6 16:35:42 显示全部楼层
madrays 发表于 2025-1-6 16:13
我自己做了  计划帮sunpanel做个美化的小项目  不着急的话可以等等哈

太好了,期待!有github地址么,学习一下
没呢 刚刚起步哈哈  详情 回复
2025-1-6 17:18

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2025-1-6 17:18:59 楼主 显示全部楼层
没呢  刚刚起步哈哈

2

主题

2

回帖

0

牛值

fnOS系统内测组

2025-1-17 14:32:39 显示全部楼层

回帖奖励 +1 飞牛币

学习了,研究研究

0

主题

4

回帖

0

牛值

江湖小虾

2025-1-24 13:49:16 显示全部楼层

回帖奖励 +1 飞牛币

主要作用是什么呢

0

主题

2

回帖

0

牛值

江湖小虾

2025-2-3 18:54:23 显示全部楼层

回帖奖励 +1 飞牛币

./conf/custom/  这个目录在哪里呢,我是绿联NAS自带docker安装的,没有找到这个目录
可以找conf,在conf下手动创建custom就行了,现在做了一个美化的项目https://github.com/madrays/sun-panel-helper 已经上线了,可以玩玩  详情 回复
2025-2-4 18:10

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2025-2-4 18:10:08 楼主 显示全部楼层
可以找conf,在conf下手动创建custom就行了,现在做了一个美化的项目https://github.com/madrays/sun-panel-helper
已经上线了,可以玩玩
好的,感谢  详情 回复
2025-2-6 08:09

0

主题

2

回帖

0

牛值

江湖小虾

2025-2-6 08:09:56 显示全部楼层
好的,感谢

3

主题

17

回帖

0

牛值

江湖小虾

2025-2-11 17:45:51 显示全部楼层

回帖奖励 +1 飞牛币

学习了,研究研究

1

主题

9

回帖

0

牛值

江湖小虾

2025-2-22 15:47:54 显示全部楼层

回帖奖励 +1 飞牛币

问下你的各种app的高清图标是哪里获取的?

0

主题

9

回帖

0

牛值

fnOS系统内测组

2025-4-2 14:41:18 显示全部楼层

回帖奖励 +1 飞牛币

好的,感谢

4

主题

54

回帖

0

牛值

初出茅庐

社区上线纪念勋章

2025-4-3 08:36:59 显示全部楼层

回帖奖励 +1 飞牛币

大佬,图标线条背景,怎么才能只让一个分组,产生效果
先弄唯一类名 再改 css 里面的类选择器  详情 回复
2025-4-3 18:48

7

主题

133

回帖

210

牛值

社区共建团

社区上线纪念勋章社区共建团荣誉勋章

2025-4-3 18:48:25 楼主 显示全部楼层
听风 发表于 2025-4-3 08:36
大佬,图标线条背景,怎么才能只让一个分组,产生效果

先弄唯一类名 再改 css 里面的类选择器
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则