本笔记介绍了如何通过 Docker 部署一个名为 Sun-Panel 的私有化 NAS 导航主页。该教程以“非牛系统”为例,采用“迷你行模式”进行快速部署,整个过程仅需复制粘贴两行代码。

目录



部署与初始化 (00:07)

本章节介绍 Sun-Panel 的完整部署流程,从连接服务器到完成基本设置。

1. 部署步骤

该教程使用 飞牛系统 作为演示平台,通过 SSH 执行 Docker 命令进行部署。

1.  连接服务器:使用 SSH 工具连接到 飞牛 服务器。
2.  切换用户:输入 susudo -i 命令切换到 root 用户权限。
3.  执行代码:复制并执行视频简介中提供的部署代码。该代码会自动拉取 Sun-Panel 的 Docker 镜像并创建容器。
4.  验证运行:代码执行成功后,回到 飞牛系统 的管理界面,可以看到 Sun-Panel 容器已处于运行状态。

2. 初始化设置

首次访问并进行基础账户配置。

1.  访问页面:在浏览器中打开 Sun-Panel 的 Web 页面。
2.  默认登录
    *   用户名[email protected]
    *   密码12345678
3.  修改账户
    *   登录后,点击右上角的 设置 图标。
    *   在账户管理中,添加一个新账户,并设置为自己熟悉的用户名和密码。
    *   保存新账户后,刷新页面并使用新账户登录。
    *   登录成功后,可返回设置将默认的 [email protected] 账户删除,以增强安全性。


核心功能配置 (01:31)

完成初始化后,可以开始对导航页进行个性化配置,添加服务和整理布局。

1. 添加容器到首页

Sun-Panel 可以集中管理和展示服务器上运行的所有 Docker 容器。

*   进入 Docker 管理:在 Sun-Panel 侧边栏点击 Docker 选项,这里会列出服务器上所有的容器。
*   添加容器
    1.  点击目标容器旁的 添加 按钮。
    2.  在弹出的窗口中,打开 显示状态 开关,以便在首页看到容器的运行状态。
    3.  将容器的访问地址(IP + 端口号,例如 http://192.168.1.10:8080)填入 默认地址 栏。
    4.  点击 在线获取图标,系统会自动抓取网站图标。
    5.  保存后,该服务就会显示在首页。
*   状态指示:正在运行的容器,其图标左上角会显示一个 绿色小圆点

2. 获取与替换图标

如果在线获取的图标不清晰或获取失败,可以手动替换。

*   推荐网站:视频中推荐了一个专门用于获取高质量应用图标的网站。
*   手动上传:在添加或编辑服务项目的窗口中,可以手动上传本地图标文件。

3. 分组管理

为了更好地组织服务,可以创建和管理分组。

*   进入分组管理:在侧边栏进入 分组管理 页面。
*   操作
    *   可以修改默认分组的名称。
    *   可以点击 添加分组 来创建新的分类,例如“影音服务”、“下载工具”等。
    *   在添加服务时,可以选择将其归入指定的分组。


高级个性化设置 (03:16)

Sun-Panel 提供了一些高级选项,用于进一步美化和定制导航页。

1. 系统状态显示

在首页添加硬件资源监控小部件。

*   进入系统状态:在侧边栏进入 系统状态 设置。
*   添加部件:可以勾选并添加 内存硬盘 的显示信息,实时监控系统资源占用情况。

2. 自定义分页

当服务项目非常多时,可以使用分页功能来保持页面整洁。

*   启用方法:此功能通过添加自定义 CSS 代码实现。
*   操作步骤
    1.  进入 个性化 -> 自定义样式 页面。
    2.  将视频简介中提供的分页代码粘贴到 CSS 编辑框中。
    3.  保存后刷新页面。
*   效果:页面左侧会出现分页选项卡,可以将不同分组的服务置于不同的页面中,使导航页更加有序。


全部命令

图标网址

1
2
3

[https://github.com/gkyang2022/dashboard-icons](https://github.com/gkyang2022/dashboard-icons)

docker命令

1
2
3
4
5
6
7
docker pull hslr/sun-panel:latest

docker run -d --restart=always -p 3002:3002 \
-v ~/docker_data/sun-panel/conf:/app/conf \
-v /var/run/docker.sock:/var/run/docker.sock \
--name sun-panel \
hslr/sun-panel:latest

自定义页脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
<script>
(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](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(42 42 42 / 90%);
        box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2);
    }

    ${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: 4px;
        margin: 15px 0;
        transition: height 0.3s ease, width 0.3s ease;
        box-shadow: 2px 0 5px rgba(0, 0, 0, 0.5);
    }

    ${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: 14px;
        color: white;
    }

    ${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)
})()
</script>