Compare commits

..

13 Commits

Author SHA1 Message Date
yuming d6daf1dce6 二次开发:补全 PRO 功能 + 优化体验
- 新增站点自定义(标题/Favicon/登录页描述)
- 新增在线自定义 CSS/JS 编辑器
- 扩展备份迁移支持面板配置导出导入
- 默认账号改为 admin/1234
- 设置按钮改为橙色更醒目
- 修复登录页误报"登录过期"弹窗
- 修复 i18n 双重 apps 块导致翻译失效

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:29:03 +08:00
红烧猎人 25f46209d9 Update README.md 2024-04-26 13:40:58 +08:00
红烧猎人 258d5de815 Update docker-build-push.yml
暂时取消自动编译,优化后重新开启
2024-02-22 09:42:50 +08:00
红烧猎人 3710f5d666 Update docker-build-push-beta.yml
暂时取消自动编译,优化后重新开启
2024-02-22 09:42:31 +08:00
Sun 917397bcf6 Merge branch 'dev' 2024-02-01 12:15:07 +08:00
Sun 8d67081a4d 更新说明文件 2024-02-01 12:14:14 +08:00
Sun 31fd128633 Merge branch 'dev' 2024-02-01 10:12:06 +08:00
Sun c9a3aa0567 优化前端版本号 2024-02-01 09:59:52 +08:00
Sun 097b23cc1e 更新版本号 2024-02-01 09:59:31 +08:00
Sun d568a31fb3 前端打包日期取消小时并设定为上海时区 2024-01-30 12:20:19 +08:00
Sun 24446260c2 Merge branch 'master' of https://github.com/hslr-s/sun-panel 2024-01-25 13:22:51 +08:00
Sun 956e645144 更新v1.3.0-beta24-01-25
Squashed commit of the following:

commit 66d0e5e94c
Author: Sun <95302870@qq.com>
Date:   Thu Jan 25 13:20:34 2024 +0800

    更新1.3.0-beta24-01-25

commit 52a81e5f55
Author: Sun <95302870@qq.com>
Date:   Thu Jan 25 13:13:08 2024 +0800

    Squashed commit of the following:

    commit 908d87357c4e22868eff85f377bb8cd22f3785d7
    Author: Sun <95302870@qq.com>
    Date:   Thu Jan 25 13:12:14 2024 +0800

        增加前端版本号

    commit 8ed2b636476c7a14ae4e1761f7712949b7ce60fc
    Author: Sun <95302870@qq.com>
    Date:   Thu Jan 25 12:29:49 2024 +0800

        增加自动获取前端版本号的git tag

commit ba7d70f9ca
Author: Sun <95302870@qq.com>
Date:   Thu Jan 25 10:58:15 2024 +0800

    优化代码

commit 104543b96f
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 21:28:31 2024 +0800

    增加系统状态组件的部分样式

commit 70c87c94c6
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 20:55:05 2024 +0800

    时间组件和搜索栏组件增加样式类型

commit 6bab5a264f
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 20:41:33 2024 +0800

    将项目列表增加样式并优化减少dom嵌套

commit 99d18df7f0
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 20:11:07 2024 +0800

    增加系统状态、logo等类名并简化部分组件dom

commit bf1cc0cc00
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 17:17:41 2024 +0800

    更新关于页面

commit fceacf58b8
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 14:54:37 2024 +0800

    增加隐藏网络模式切换开关

commit e0dcc49f5b
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 13:55:28 2024 +0800

    优化部分错误码

commit 863cdf0fc1
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 12:54:55 2024 +0800

    声明暴露端口

commit 5b25ef9c19
Author: Sun <95302870@qq.com>
Date:   Wed Jan 24 12:38:48 2024 +0800

    修改命令行所有的输出内容为英文
2024-01-25 13:21:47 +08:00
红烧猎人 f89879eead Update issue templates 2024-01-24 17:54:03 +08:00
32 changed files with 652 additions and 201 deletions
+3 -3
View File
@@ -7,11 +7,11 @@ assignees: ''
---
** Version | 版本 **
**Version | 版本**
eg: v1.0.0
** Environment | 运行环境 **
**Environment | 运行环境**
eg: docker / ubuntu / windows
** Detailed description | 详细说明 **
**Detailed description | 详细说明**
...
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] Short description | 简短说明,每条issue最好写一个建议,不要重复提出"
title: "[Feature] Short description | 简短说明"
labels: enhancement
assignees: ''
+4 -3
View File
@@ -2,9 +2,10 @@ name: docker-push-beta
on:
workflow_dispatch:
push:
tags:
- 'v*'
# 暂时取消自动编译,优化后重新开启
# push:
# tags:
# - 'v*'
jobs:
build:
+4 -3
View File
@@ -2,9 +2,10 @@ name: docker-push
on:
workflow_dispatch:
push:
tags:
- 'v*'
# 暂时取消自动编译,优化后重新开启
# push:
# tags:
# - 'v*'
jobs:
build:
+86
View File
@@ -0,0 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目简介
Sun-Panel 是服务器/NAS 导航面板应用,前后端分离架构:
- **前端**Vue 3 + TypeScript + Vite + Naive UI + Pinia
- **后端**Go + Gin + GORM,支持 SQLite(默认)或 MySQL,可选 Redis
## 常用命令
### 前端
```bash
pnpm dev # 启动开发服务器,端口 1002
pnpm build # 生产构建(类型检查 + Vite 打包)
pnpm lint # ESLint 检查
pnpm lint:fix # 自动修复
pnpm type-check # 仅 TypeScript 类型检查
```
### 后端
```bash
cd service
go run main.go # 启动,默认端口 3002
```
### Docker 全量构建
```bash
docker build -t sun-panel .
```
## 架构要点
### 前后端通信
开发时,Vite 将 `/api/*``/uploads/*` 代理到 `http://127.0.0.1:3002`(由 `.env``VITE_APP_API_BASE_URL` 控制)。生产部署时前后端同端口,Go 直接 serve 静态文件。
### 前端分层
```
src/api/ → API 调用函数(按模块分:panel/ system/ 等)
src/store/ → Pinia 状态(auth/user/panel/admin/notice/moduleConfig/app
src/views/ → 页面组件(home/login/exception
src/components/ → UI 组件(apps/ common/ deskModule/
src/utils/request/ → axios 封装,含 token 拦截器
src/hooks/ → Composition APIuseTheme/useLanguage/useBasicLayout/useIconRender
src/locales/ → 国际化(zh-CN.json / en-US.json
```
### 后端分层
```
service/router/ → 路由注册,入口 A_ENTER.go
service/api/api_v1/ → Handler 层,中间件在 middleware/
service/lib/ → 业务逻辑(user/cache/monitor/siteFavicon 等)
service/models/ → GORM 模型
service/global/ → 全局变量(Db/Logger/Redis 等)
service/initialize/ → 启动初始化流程,入口 A_ENTER.go
```
后端每个子目录都以 `A_ENTER.go` 作为该模块的入口聚合文件。
### 路由分组(后端)
- `/system/*` — 登录/用户/系统设置,需 LoginInterceptor
- `/panel/*` — 面板数据/图标管理,需 LoginInterceptor
- `/openness/*` — 无需认证的公开接口
### 配置文件
后端使用 INI 格式配置,示例见 `service/assets/conf.example.ini`。关键字段:
- `database_drive``sqlite`(默认)或 `mysql`
- `cache_drive` / `queue_drive``memory`(默认)或 `redis`
- `source_path`:上传文件存储路径,默认 `./uploads`
## 开发注意
- 前端路径别名 `@` 指向 `src/`
- 提交前 Husky + lint-staged 会自动对 `.ts/.tsx/.vue` 运行 `eslint --fix`
- TypeScript 开启 `strict``noUnusedLocals`,避免引入 `any`
- 国际化新增文案需同时更新 `zh-CN.json``en-US.json`
+1 -1
View File
@@ -4,7 +4,7 @@ FROM node AS web_image
# 华为源
# RUN npm config set registry https://repo.huaweicloud.com/repository/npm/
RUN npm install pnpm -g
RUN npm install pnpm@8 -g
WORKDIR /build
+49 -143
View File
@@ -21,57 +21,57 @@
[[ Document ]](https://sun-panel-doc.enianteam.com) |
[[ Demo ]](http://sunpaneldemo.enianteam.com)
Server, NAS navigation panel, Homepage, Browser homepage.
A server, NAS navigation panel, Homepage, Browser homepage.
<br>
一个服务器、NAS导航面板、Homepage、浏览器首页。
</div>
<!-- <img src="./doc/images/logo.png" align="left" width="180px" height="180px"/>
<img align="left" width="0" height="192px" hspace="10"/>
# Sun-Panel
Server, NAS navigation panel, Homepage, Browser homepage.
<br>
一个服务器、NAS导航面板、Homepage、浏览器首页。 -->
![](./doc/images/main-dark.png)
> [!IMPORTANT]
> In order to maintain the livelihood, the author added some [`PRO`] (https://pro.sun-panel.top) function, so the project temporarily entered a closed source state.; At present, the latest version of the open source is `v1.3.0`, [Please see the latest version of closed source](https://github.com/hslr-s/sun-panel/releases).; When the modular technology is developed, the separation of the PRO and the programs will be opened again, and the closed source will have no effect on ordinary users.; Let's look forward to open source again, and at the same time, we are welcome to supervise and review the security of the program.
>
> 作者为了维持生计,增加了一些 [`PRO`](https://pro.sun-panel.top) 功能,所以项目暂时进入闭源状态。目前开源最新版本为`v1.3.0`,[闭源最新版本请查看](https://github.com/hslr-s/sun-panel/releases)。待开发出模块化技术,然后对PRO和主程序进行分离会再次开源,闭源对普通用户没有任何影响。我们一起期待再次开源吧,同时也欢迎各位大佬对程序的安全性进行监督和审查。
## 😎 Features
- 🍉 Clean interface, powerful functionality, low resource consumption
- 🍊 Easy to use, visual operation, zero-code usage
- 🍠 One-click switch between internal and external network modes
- 🍵 Supports Docker deployment (compatible with Arm systems)
- 🎪 Supports multi-account isolation
- 🎏 Supports viewing system status
- 🫙 Supports custom JS, CSS
- 🍻 Simple usage without the need to connect to an external database
- 🍾 Rich icon styles for free combination, supports [Iconify icon library](https://icon-sets.iconify.design/)
- 🚁 Supports opening small windows in the webpage (some third-party websites may block this feature)
## 🖼️ Preview Screenshots
**Various styles, freely combined**
![](./doc/images/icon-small-new.png)
![](./doc/images/transparent-info.png)
![](./doc/images/transparent-small.png)
![](./doc/images/solid-color-info.png)
![](./doc/images/full-color-small.jpg)
**Built-in small windows**
![](./doc/images/window-ssh.png)
![](./doc/images/window-xunlei.png)
## 🐳 Deployment tutorial
[Deployment Tutorial](https://sun-panel-doc.enianteam.com/usage/quick_deploy.html)
![](./doc/images/icon-info-new.png)
## 🍵 Donate
## 😎 特点
> Open-source development is not easy. If you feel that my project has helped you, you are welcome to [donate](./doc/donate.md) or buy me a cup of tea☕ (please leave your nickname or name in the note if possible). Your support is my motivation, thank you.
- 简洁
- 局域网内外网链接切换
- docker部署,对arm系统支持
- 上手简单,免修改代码
- 无需连接外部数据库
- 丰富图标自由搭配(文字图标+svg图标+内置三方图标库)
- 支持网页内置小窗口打开(部分网站屏蔽此功能)
- 占用资源小
## 🧊 最新完整文档(DOC
[最新完整文档(DOC](https://sun-panel-doc.enianteam.com/)
## 🎨 演示(demo
[查看演示站](https://sun-panel-doc.enianteam.com/introduce/demo_site.html)
## 🐳 交流群&社区
开发者:**[红烧猎人](https://blog.enianteam.com/u/sun/content/11)**
QQ交流群,进不去可以点上方连接联系作者
<img src="./doc/images/qq_group_qr2.png" height="350" />
Github社区板块:https://github.com/hslr-s/sun-panel/discussions
## 🍵 打赏
> 开源开发不易,如果觉得我的项目有帮到你,欢迎给我[打赏](./doc/donate.md)或者请我喝个奶茶☕(如果可以备注下您的昵称或者名字),你的支持就是我的动力,谢谢。
<a href="https://www.paypal.me/hslrs">
<img height="60" src="./doc/images/donate/paypal.png" target="_blank"></img>
@@ -82,117 +82,23 @@ Github社区板块:https://github.com/hslr-s/sun-panel/discussions
| ------------ | ------------ |
| <img height="300" src="./doc/images/donate/weixin.png"/> | <img height="300" src="./doc/images/donate/alipay.png" /> |
## 🫓 TODO
## 🏖️ Communication group & community
- [x] 分组,拖拽排序
- [x] 导入导出功能
- [x] 增加访客账号
- [x] 帐号解除邮箱限制
- [x] 对上传的文件管理(针对账户增强重复利用,节省空间)
- [x] 服务器监控
- [x] 多国语言支持
- [ ] 用户自定义搜索框搜索引擎
- [ ] 搜索框样式自定义(背景颜色,文字颜色)
- [ ] docker管理器
- [ ] 计划任务
Author**[红烧猎人](https://blog.enianteam.com/u/sun/content/11)**
[Github Discussions](https://github.com/hslr-s/sun-panel/discussions)
QQ交流群,进不去可以点上方连接联系作者
## 🖼️ 预览截图
<img src="./doc/images/qq_group_qr2.png" height="350" />
**各种风格,自由搭配**
![](./doc/images/icon-small-new.png)
![](./doc/images/transparent-info.png)
![](./doc/images/transparent-small.png)
![](./doc/images/solid-color-info.png)
![](./doc/images/full-color-small.jpg)
**内置小窗口**
![](./doc/images/window-ssh.png)
![](./doc/images/window-xunlei.png)
## 🍜 使用运行教程
<div id="default-username"></div>
### 默认账号密码
账号:admin@sun.cc
密码:12345678
### 命令参数
|参数|说明|
|---|---|
|-h|查看命令说明|
|-config|生成配置文件(conf/conf.ini|
|-password-reset|重置第一个用户的密码|
### 二进制文件运行
去 [Releases](https://github.com/hslr-s/sun-panel/releases) 下载二进制文件
执行示例
```sh
./sun-panel
```
#### 重置密码
执行示例
```sh
./sun-panel -password-reset
```
输出
```
密码已经重置成功,以下是账号信息
用户名 xxx@qq.com
密码 12345678
```
### docker 运行
目录挂载 `-v`,根据自己的需求选择:
|容器目录|说明|
|---|---|
|/app/conf|配置文件|
|/app/uploads|上传的文件|
|/app/database|数据库文件|
|/app/runtime|运行日志(不推荐挂载)|
1. 拉取镜像
```sh
docker pull hslr/sun-panel
```
2. 直接下载运行
```sh
docker run -d --restart=always -p 3002:3002 \
-v ~/docker_data/sun-panel/conf:/app/conf \
-v ~/docker_data/sun-panel/uploads:/app/uploads \
-v ~/docker_data/sun-panel/database:/app/database \
--name sun-panel \
hslr/sun-panel
```
### 自编译运行
[请参考完整文档](https://sun-panel-doc.enianteam.com/zh_cn/usage/compile.html)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=hslr-s/sun-panel&type=Date)](https://star-history.com/#hslr-s/sun-panel&Date)
## ❤️ 感谢
## ❤️ Thanks
- [Roc](https://github.com/RocCheng)
- [jackloves111](https://github.com/jackloves111)
- [Rock.L](https://github.com/gitlyp)
## LICENSE
[MIT](./LICENSE)
---
[![Star History Chart](https://api.star-history.com/svg?repos=hslr-s/sun-panel&type=Date)](https://star-history.com/#hslr-s/sun-panel&Date)
+3 -1
View File
@@ -4,7 +4,9 @@ const moment = require('moment')
// git 最新标签
// const latestTag = execSync('git describe --tags --abbrev=0').toString().trim()
const packDate = moment().format('YYYYMMDD-HH')
// 设置默认时区为 'Asia/Shanghai'
const packDate = moment().utc().format('YYYYMMDD')
// 要追加的内容
const contentToAppend = `\nVITE_APP_VERSION=${packDate}`
Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

+3
View File
@@ -68,5 +68,8 @@
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
},
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "vue-demi"]
}
}
+9 -7
View File
@@ -1,11 +1,13 @@
package system
type ApiSystem struct {
About About
LoginApi LoginApi
UserApi UserApi
FileApi FileApi
NoticeApi NoticeApi
ModuleConfigApi ModuleConfigApi
MonitorApi MonitorApi
About About
LoginApi LoginApi
UserApi UserApi
FileApi FileApi
NoticeApi NoticeApi
ModuleConfigApi ModuleConfigApi
MonitorApi MonitorApi
SiteCustomizeApi SiteCustomizeApi
CustomStyleApi CustomStyleApi
}
+33
View File
@@ -0,0 +1,33 @@
package system
import (
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/global"
"sun-panel/lib/cmn/systemSetting"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type CustomStyleApi struct{}
// 获取自定义 CSS/JS(无需登录)
func (a *CustomStyleApi) Get(c *gin.Context) {
cfg := systemSetting.CustomStyle{}
global.SystemSetting.GetValueByInterface(systemSetting.CUSTOM_STYLE, &cfg)
apiReturn.SuccessData(c, cfg)
}
// 保存自定义 CSS/JS(管理员)
func (a *CustomStyleApi) Set(c *gin.Context) {
cfg := systemSetting.CustomStyle{}
if err := c.ShouldBindBodyWith(&cfg, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
if err := global.SystemSetting.Set(systemSetting.CUSTOM_STYLE, cfg); err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
apiReturn.Success(c)
}
@@ -0,0 +1,33 @@
package system
import (
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/global"
"sun-panel/lib/cmn/systemSetting"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type SiteCustomizeApi struct{}
// 获取站点自定义配置(无需登录,登录页使用)
func (a *SiteCustomizeApi) Get(c *gin.Context) {
cfg := systemSetting.SiteCustomize{}
global.SystemSetting.GetValueByInterface(systemSetting.SITE_CUSTOMIZE, &cfg)
apiReturn.SuccessData(c, cfg)
}
// 保存站点自定义配置(管理员)
func (a *SiteCustomizeApi) Set(c *gin.Context) {
cfg := systemSetting.SiteCustomize{}
if err := c.ShouldBindBodyWith(&cfg, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
if err := global.SystemSetting.Set(systemSetting.SITE_CUSTOMIZE, cfg); err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
apiReturn.Success(c)
}
+1 -1
View File
@@ -1 +1 @@
9|1.3.0-beta24-01-25
10|1.3.0
+2 -2
View File
@@ -135,13 +135,13 @@ func NotFoundAndCreateUser(db *gorm.DB) error {
if err != gorm.ErrRecordNotFound {
return err
}
username := "admin@sun.cc"
username := "admin"
fUser.Mail = username
fUser.Username = username
fUser.Name = username
fUser.Status = 1
fUser.Role = 1
fUser.Password = cmn.PasswordEncryption("12345678")
fUser.Password = cmn.PasswordEncryption("1234")
if errCreate := db.Create(&fUser).Error; errCreate != nil {
return errCreate
@@ -15,6 +15,8 @@ const (
DISCLAIMER = "disclaimer" // 免责声明 储存类型:字符串
WEB_ABOUT_DESCRIPTION = "web_about_description" // 关于的描述信息
PANEL_PUBLIC_USER_ID = "panel_public_user_id" // 公开访问模式用户id *uint|null
SITE_CUSTOMIZE = "site_customize" // 站点自定义配置
CUSTOM_STYLE = "custom_style" // 自定义 CSS/JS
)
type SystemSettingCache struct {
@@ -43,6 +45,19 @@ type ApplicationSetting struct {
WebSiteUrl string `json:"webSiteUrl"` // 站点地址
}
// 站点自定义配置
type SiteCustomize struct {
SiteTitle string `json:"siteTitle"` // 站点标题(显示在登录页)
FaviconUrl string `json:"faviconUrl"` // Favicon 图标 URL
LoginDescription string `json:"loginDescription"` // 登录页描述文字
}
// 自定义 CSS/JS
type CustomStyle struct {
Css string `json:"css"` // 自定义 CSS 内容
Js string `json:"js"` // 自定义 JS 内容
}
var (
ErrorNoExists = errors.New("no exists")
)
+2
View File
@@ -10,4 +10,6 @@ func Init(routerGroup *gin.RouterGroup) {
InitNoticeRouter(routerGroup)
InitModuleConfigRouter(routerGroup)
InitMonitorRouter(routerGroup)
InitSiteCustomizeRouter(routerGroup)
InitCustomStyleRouter(routerGroup)
}
+19
View File
@@ -0,0 +1,19 @@
package system
import (
"sun-panel/api/api_v1"
"sun-panel/api/api_v1/middleware"
"github.com/gin-gonic/gin"
)
func InitCustomStyleRouter(router *gin.RouterGroup) {
api := api_v1.ApiGroupApp.ApiSystem.CustomStyleApi
// 无需登录
router.POST("/system/customStyle/get", api.Get)
// 管理员才能修改
rAdmin := router.Group("", middleware.LoginInterceptor, middleware.AdminInterceptor)
rAdmin.POST("/system/customStyle/set", api.Set)
}
+19
View File
@@ -0,0 +1,19 @@
package system
import (
"sun-panel/api/api_v1"
"sun-panel/api/api_v1/middleware"
"github.com/gin-gonic/gin"
)
func InitSiteCustomizeRouter(router *gin.RouterGroup) {
api := api_v1.ApiGroupApp.ApiSystem.SiteCustomizeApi
// 无需登录(登录页读取)
router.POST("/system/siteCustomize/get", api.Get)
// 管理员才能修改
rAdmin := router.Group("", middleware.LoginInterceptor, middleware.AdminInterceptor)
rAdmin.POST("/system/siteCustomize/set", api.Set)
}
+25
View File
@@ -1,11 +1,36 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { NConfigProvider } from 'naive-ui'
import { NaiveProvider } from '@/components/common'
import { useTheme } from '@/hooks/useTheme'
import { useLanguage } from '@/hooks/useLanguage'
import { getCustomStyle } from '@/api/system/customStyle'
import type { CustomStyle } from '@/api/system/customStyle'
const { theme, themeOverrides } = useTheme()
const { language } = useLanguage()
// 注入自定义 CSS/JS
onMounted(async () => {
try {
const res = await getCustomStyle<CustomStyle>()
if (res.code === 0 && res.data) {
if (res.data.css) {
const style = document.createElement('style')
style.id = 'sun-panel-custom-css'
style.textContent = res.data.css
document.head.appendChild(style)
}
if (res.data.js) {
const script = document.createElement('script')
script.id = 'sun-panel-custom-js'
script.textContent = res.data.js
document.body.appendChild(script)
}
}
}
catch {}
})
</script>
<template>
+19
View File
@@ -0,0 +1,19 @@
import { post } from '@/utils/request'
export interface CustomStyle {
css: string
js: string
}
export function getCustomStyle<T>() {
return post<T>({
url: '/system/customStyle/get',
})
}
export function setCustomStyle<T>(data: CustomStyle) {
return post<T>({
url: '/system/customStyle/set',
data,
})
}
+20
View File
@@ -0,0 +1,20 @@
import { post } from '@/utils/request'
export interface SiteCustomize {
siteTitle: string
faviconUrl: string
loginDescription: string
}
export function getSiteCustomize<T>() {
return post<T>({
url: '/system/siteCustomize/get',
})
}
export function setSiteCustomize<T>(data: SiteCustomize) {
return post<T>({
url: '/system/siteCustomize/set',
data,
})
}
+70
View File
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NButton, NForm, NFormItem, NInput, NText, useMessage } from 'naive-ui'
import { getCustomStyle, setCustomStyle } from '@/api/system/customStyle'
import type { CustomStyle } from '@/api/system/customStyle'
import { t } from '@/locales'
const ms = useMessage()
const saving = ref(false)
const form = ref<CustomStyle>({
css: '',
js: '',
})
onMounted(async () => {
try {
const res = await getCustomStyle<CustomStyle>()
if (res.code === 0 && res.data)
form.value = res.data
}
catch {}
})
async function handleSave() {
saving.value = true
try {
const res = await setCustomStyle<any>(form.value)
if (res.code === 0)
ms.success(t('apps.customStyle.saveSuccess'))
}
catch {}
finally {
saving.value = false
}
}
</script>
<template>
<div class="p-4">
<NText depth="3" class="block mb-3 text-sm">
{{ t('apps.customStyle.tip') }}
</NText>
<NForm label-placement="top">
<NFormItem :label="t('apps.customStyle.cssLabel')">
<NInput
v-model:value="form.css"
type="textarea"
:rows="10"
:placeholder="t('apps.customStyle.cssPlaceholder')"
style="font-family: monospace; font-size: 13px;"
/>
</NFormItem>
<NFormItem :label="t('apps.customStyle.jsLabel')">
<NInput
v-model:value="form.js"
type="textarea"
:rows="10"
:placeholder="t('apps.customStyle.jsPlaceholder')"
style="font-family: monospace; font-size: 13px;"
/>
</NFormItem>
<NFormItem>
<NButton type="primary" :loading="saving" @click="handleSave">
{{ t('common.save') }}
</NButton>
</NFormItem>
</NForm>
</div>
</template>
+47 -10
View File
@@ -8,6 +8,7 @@ import { ConfigVersionLowError, FormatError, exportJson, importJsonString } from
import { get as getAbout } from '@/api/system/about'
import { edit as addGroup, getList as getGroupList } from '@/api/panel/itemIconGroup'
import { addMultiple as addMultipleIcons, getListByGroupId } from '@/api/panel/itemIcon'
import { get as getUserConfig, set as setUserConfig } from '@/api/panel/userConfig'
import { t } from '@/locales'
@@ -28,8 +29,8 @@ const debug = ref(false)
const importObj = ref<ImportJsonResult | null> (null)
const importItems = ref<string[]>(['icons']) // 当前软件版本支持导入导出的项目
const checkedItems = ref<string[]>(['icons']) // 当前准备导入的项目
const importItems = ref<string[]>(['icons', 'userConfig']) // 当前软件版本支持导入导出的项目
const checkedItems = ref<string[]>(['icons', 'userConfig']) // 当前准备导入的项目
// 导入图标
async function importIcons(): Promise<string | null> {
@@ -210,36 +211,70 @@ function importCheck() {
}
}
// 导出面板配置
async function exportUserConfig(): Promise<Panel.userConfig | null> {
try {
const res = await getUserConfig<Panel.userConfig>()
if (res.code === 0)
return res.data
}
catch {}
return null
}
// 导入面板配置
async function importUserConfig(): Promise<string | null> {
const cfg = importObj.value?.getUserConfig()
if (!cfg)
return null
try {
const res = await setUserConfig<any>(cfg)
if (res.code !== 0)
return res.msg
}
catch (error) {
if (error instanceof Error)
return error.message
}
return null
}
// 开始导出
async function handleStartExport() {
loading.value = true
// console.log('要导出的项目', checkedItems.value)
// 获取软件版本号
const exportResult = exportJson(version.value)
if (checkedItems.value.includes('icons')) {
console.log('export icons ...')
const iconGroups = await exportIcons()
exportResult.addIconsData(iconGroups)
console.log('export icons finish', iconGroups)
}
// console.log('导出结果')
if (checkedItems.value.includes('userConfig')) {
const cfg = await exportUserConfig()
if (cfg)
exportResult.addUserConfigData(cfg)
}
jsonData.value = exportResult.string()
exportResult.exportFile()
loading.value = false
exportRoundModalShow.value = false
// ms.success(t('common.success'))
}
// 开始导入
async function handleStartImport() {
loading.value = true
if (checkedItems.value.includes('icons')) {
console.log('export icons ...')
const errMsg = await importIcons()
if (errMsg !== null)
ms.success(`${t('common.failed')}:${errMsg}`)
ms.error(`${t('common.failed')}:${errMsg}`)
}
if (checkedItems.value.includes('userConfig')) {
const errMsg = await importUserConfig()
if (errMsg !== null)
ms.error(`${t('common.failed')}:${errMsg}`)
}
loading.value = false
@@ -325,6 +360,7 @@ async function handleStartImport() {
<NSpace justify="center" style="margin-top: 20px;">
<NCheckboxGroup v-model:value="checkedItems">
<NCheckbox v-if="importItems.includes('icons')" value="icons" :label="$t('apps.exportImport.moduleIcon')" />
<NCheckbox v-if="importItems.includes('userConfig')" value="userConfig" :label="$t('apps.exportImport.moduleUserConfig')" />
<NCheckbox v-if="importItems.includes('style')" value="style" :label="$t('apps.exportImport.moduleStyle')" />
</NCheckboxGroup>
</NSpace>
@@ -345,6 +381,7 @@ async function handleStartImport() {
<NSpace justify="center" style="margin-top: 20px;">
<NCheckboxGroup v-model:value="checkedItems">
<NCheckbox v-if="importItems.includes('icons')" value="icons" :label="$t('apps.exportImport.moduleIcon')" />
<NCheckbox v-if="importItems.includes('userConfig')" value="userConfig" :label="$t('apps.exportImport.moduleUserConfig')" />
<NCheckbox v-if="importItems.includes('style')" value="style" :label="$t('apps.exportImport.moduleStyle')" />
</NCheckboxGroup>
</NSpace>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui'
import { getSiteCustomize, setSiteCustomize } from '@/api/system/siteCustomize'
import type { SiteCustomize } from '@/api/system/siteCustomize'
import { t } from '@/locales'
const ms = useMessage()
const saving = ref(false)
const form = ref<SiteCustomize>({
siteTitle: '',
faviconUrl: '',
loginDescription: '',
})
onMounted(async () => {
try {
const res = await getSiteCustomize<SiteCustomize>()
if (res.code === 0 && res.data)
form.value = res.data
}
catch {}
})
async function handleSave() {
saving.value = true
try {
const res = await setSiteCustomize<any>(form.value)
if (res.code === 0)
ms.success(t('apps.siteCustomize.saveSuccess'))
}
catch {}
finally {
saving.value = false
}
}
</script>
<template>
<div class="p-4">
<NForm label-placement="top">
<NFormItem :label="t('apps.siteCustomize.siteTitle')">
<NInput
v-model:value="form.siteTitle"
:placeholder="t('apps.siteCustomize.siteTitlePlaceholder')"
/>
</NFormItem>
<NFormItem :label="t('apps.siteCustomize.faviconUrl')">
<NInput
v-model:value="form.faviconUrl"
:placeholder="t('apps.siteCustomize.faviconUrlPlaceholder')"
/>
</NFormItem>
<NFormItem :label="t('apps.siteCustomize.loginDescription')">
<NInput
v-model:value="form.loginDescription"
type="textarea"
:rows="3"
:placeholder="t('apps.siteCustomize.loginDescriptionPlaceholder')"
/>
</NFormItem>
<NFormItem>
<NButton type="primary" :loading="saving" @click="handleSave">
{{ t('common.save') }}
</NButton>
</NFormItem>
</NForm>
</div>
</template>
+20
View File
@@ -90,6 +90,7 @@
"fileModified": "The file has been modified, import with caution",
"import": "Import configuration",
"moduleIcon": "Icon configuration",
"moduleUserConfig": "Panel configuration",
"moduleStyle": "Style configuration",
"selectExportData": "Select the configuration data to export",
"selectImportData": "Select the configuration data to import",
@@ -125,6 +126,25 @@
"dark": "Dark ",
"light": "Light "
}
},
"siteCustomize": {
"appName": "Site Customize",
"siteTitle": "Site Title",
"siteTitlePlaceholder": "Leave blank to use default title Sun-Panel",
"faviconUrl": "Favicon URL",
"faviconUrlPlaceholder": "Leave blank to use default icon",
"loginDescription": "Login Page Description",
"loginDescriptionPlaceholder": "Leave blank to hide",
"saveSuccess": "Saved successfully"
},
"customStyle": {
"appName": "Custom CSS/JS",
"cssLabel": "Custom CSS",
"jsLabel": "Custom JS",
"cssPlaceholder": "/* Enter custom CSS here */",
"jsPlaceholder": "// Enter custom JS here",
"saveSuccess": "Saved successfully",
"tip": "Refresh the page after saving to apply changes"
}
},
"common": {
+20
View File
@@ -90,6 +90,7 @@
"fileModified": "文件被修改过,谨慎导入",
"import": "导入配置",
"moduleIcon": "图标配置",
"moduleUserConfig": "面板配置",
"moduleStyle": "样式配置",
"selectExportData": "请选择要导出的配置数据",
"selectImportData": "请选择要导入的配置数据",
@@ -125,6 +126,25 @@
"dark": "深色 ",
"light": "浅色 "
}
},
"siteCustomize": {
"appName": "站点自定义",
"siteTitle": "站点标题",
"siteTitlePlaceholder": "留空则使用默认标题 Sun-Panel",
"faviconUrl": "Favicon 图标地址",
"faviconUrlPlaceholder": "留空则使用默认图标",
"loginDescription": "登录页描述",
"loginDescriptionPlaceholder": "留空不显示",
"saveSuccess": "保存成功"
},
"customStyle": {
"appName": "自定义 CSS/JS",
"cssLabel": "自定义 CSS",
"jsLabel": "自定义 JS",
"cssPlaceholder": "/* 在此输入自定义 CSS */",
"jsPlaceholder": "// 在此输入自定义 JS",
"saveSuccess": "保存成功",
"tip": "修改后刷新页面生效"
}
},
"common": {
+13 -2
View File
@@ -25,7 +25,7 @@ export interface JsonStructure {
exportTime: string
appVersion: string
icons?: any
// styleConfig: Panel.panelConfig
userConfig?: Panel.userConfig
md5: string
}
@@ -49,6 +49,7 @@ export interface IconGroup {
interface ExportJsonResult {
addIconsData(datas: IconGroup[]): ExportJsonResult
addUserConfigData(data: Panel.userConfig): ExportJsonResult
exportFile(): void
string(): string
}
@@ -75,6 +76,12 @@ export function exportJson(appVersion?: string): ExportJsonResult {
return this
},
// 添加用户面板配置
addUserConfigData(data: Panel.userConfig) {
jsonData.userConfig = data
return this
},
// 导出json文件
exportFile() {
generateMD5AndUpdate()
@@ -103,7 +110,8 @@ export interface ImportJsonResult {
isPassCheckConfigVersionBest: () => boolean // 验证程序的导入版本驱动是否为最佳 当配置文件和驱动版本相等的时候为最佳,否则不匹配过新或者过旧
jsonStruct: JsonStructure // 根据实际情况提供更具体的类型定义
hasProperty: (key: string) => boolean
geticons: () => IconGroup[] // 根据实际情况提供更具体的类型定义
geticons: () => IconGroup[]
getUserConfig: () => Panel.userConfig | null
}
// 导入json数据
@@ -140,6 +148,9 @@ export function importJsonString(jsonString: string): ImportJsonResult | null {
geticons: (): IconGroup[] => {
return jsonStruct.icons || []
},
getUserConfig: (): Panel.userConfig | null => {
return jsonStruct.userConfig || null
},
}
}
+12 -12
View File
@@ -35,19 +35,19 @@ function http<T = any>(
return res.data
if (res.data.code === 1001) {
// 避免重复弹窗
if (loginMessageShow === false) {
loginMessageShow = true
message.warning(t('api.loginExpires'), {
// message.warning('登录过期', {
onLeave() {
loginMessageShow = false
},
})
}
router.push({ path: '/login' })
authStore.removeToken()
// 已在登录页不弹提示,避免带旧 token 进入时误报
if (router.currentRoute.value.path !== '/login') {
if (loginMessageShow === false) {
loginMessageShow = true
message.warning(t('api.loginExpires'), {
onLeave() {
loginMessageShow = false
},
})
}
router.push({ path: '/login' })
}
return res.data
}
+19 -8
View File
@@ -92,15 +92,26 @@ function handleResize() {
}
onMounted(() => {
const adminApp: App = {
name: t('adminSettingUsers.appName'),
componentName: 'Users',
icon: 'lucide-users',
auth: 1,
if (authStore.userInfo?.role === 1) {
apps.value.push({
name: t('adminSettingUsers.appName'),
componentName: 'Users',
icon: 'lucide-users',
auth: 1,
})
apps.value.push({
name: t('apps.siteCustomize.appName'),
componentName: 'SiteCustomize',
icon: 'mdi-earth-settings',
auth: 1,
})
apps.value.push({
name: t('apps.customStyle.appName'),
componentName: 'CustomStyle',
icon: 'mdi-code-braces',
auth: 1,
})
}
// 初始化
if (authStore.userInfo?.role === 1)
apps.value.push(adminApp)
window.addEventListener('resize', handleResize)
handleResize()
+1 -1
View File
@@ -524,7 +524,7 @@ function handleAddItem(itemIconGroupId?: number) {
</template>
</NButton>
<NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN" color="#2a2a2a6b" @click="settingModalShow = !settingModalShow">
<NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN" color="#f97316" @click="settingModalShow = !settingModalShow">
<template #icon>
<SvgIcon class="text-white font-xl" icon="majesticons-applications" />
</template>
+29 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { NButton, NCard, NForm, NFormItem, NGradientText, NInput, NSelect, useMessage } from 'naive-ui'
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { login } from '@/api'
import { useAppStore, useAuthStore } from '@/store'
import { SvgIcon } from '@/components/common'
@@ -8,14 +8,37 @@ import { router } from '@/router'
import { t } from '@/locales'
import { languageOptions } from '@/utils/defaultData'
import type { Language } from '@/store/modules/app/helper'
import { getSiteCustomize } from '@/api/system/siteCustomize'
import type { SiteCustomize } from '@/api/system/siteCustomize'
// const userStore = useUserStore()
const authStore = useAuthStore()
const appStore = useAppStore()
const ms = useMessage()
const loading = ref(false)
const languageValue = ref<Language>(appStore.language)
const siteCustomize = ref<SiteCustomize>({
siteTitle: '',
faviconUrl: '',
loginDescription: '',
})
onMounted(async () => {
try {
const res = await getSiteCustomize<SiteCustomize>()
if (res.code === 0 && res.data) {
siteCustomize.value = res.data
// 动态设置 favicon
if (res.data.faviconUrl) {
const link = document.querySelector<HTMLLinkElement>('link[rel~="icon"]')
if (link)
link.href = res.data.faviconUrl
}
}
}
catch {}
})
// const isShowCaptcha = ref<boolean>(false)
// const isShowRegister = ref<boolean>(false)
@@ -75,9 +98,12 @@ function handleChangeLanuage(value: Language) {
<div class="login-title ">
<NGradientText :size="30" type="success" class="!font-bold">
{{ $t('common.appName') }}
{{ siteCustomize.siteTitle || $t('common.appName') }}
</NGradientText>
</div>
<div v-if="siteCustomize.loginDescription" class="text-center text-slate-400 text-sm mb-3">
{{ siteCustomize.loginDescription }}
</div>
<NForm :model="form" label-width="100px" @keydown.enter="handleSubmit">
<NFormItem>
<NInput v-model:value="form.username" :placeholder="$t('login.usernamePlaceholder')">