更新v1.1.0

Squashed commit of the following:

commit 4443f7c8251b31687ed93114930ab3d769f4ed6c
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 22:10:49 2023 +0800

    美化关于页

commit 95ca46d460eba469ca8ae54f65c7773835061c0f
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 21:59:48 2023 +0800

    更新版本号,更新说明文件增加新版预览截图

commit 052e5f81fe4065e10199d52bc041329fc9c5fe86
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 21:06:52 2023 +0800

    修复后端mkdirAll权限的问题

commit ace57d5ba69c311e40997d5791cf03a8b28e0c07
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 20:59:35 2023 +0800

    修改配置文件

commit 099015f2767cedfd6eae91e60131817471eb1f24
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 14:10:39 2023 +0800

    增加docker-compose文件

commit e229003431ff2476f0ab63a8dffb88504716ba48
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 13:53:20 2023 +0800

    提交更新日志文件

commit e8736b8b62db6d590c063b42757599381429541e
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 13:49:38 2023 +0800

    增加隐藏小图标

commit 038af3aaa91a023cc10aabff5b0cfd15c64d0b46
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 13:49:04 2023 +0800

    优化 密码限制

commit 4cd15a383923bf3de56e9e4dc6df3bf97236ed18
Author: Sun <95302870@qq.com>
Date:   Tue Nov 28 12:34:59 2023 +0800

    增加反馈入口

commit daf6aea902893f816dca5c0bb09326f2f110ddcc
Merge: 3edfadd b057e25
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 22:19:59 2023 +0800

    Merge branch 'master' into dev

commit 3edfaddd173efcbbde867dc9ce9b022920b39061
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 22:17:08 2023 +0800

    修改docker的编译镜像和运行镜像为alpine,兼容极空间设备

commit 3445f97152c2f6b9f1f9f68b46e4b84f8240c9c2
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 13:58:59 2023 +0800

    修复前端编译错误

commit 3ef02013ffb595e7805692350389bb623155cfe9
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 13:56:12 2023 +0800

    更新beta版本号

commit 620f0f1e1523f34e87001e8a2bbe3d4a01cb0b9b
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 13:53:46 2023 +0800

    修复 添加图标成功后遗留旧数据的问题

commit 55d877d1ca11e83d9f7325a321aeb5b65ad4ee8b
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 13:41:55 2023 +0800

    增加置顶按钮

commit f28dd63328aeca5d8c3c036c5786304f8a33b1f9
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 12:56:14 2023 +0800

    优化roundmodal的样式和手机端设置样式

commit c19ce176878ea2ef06b0a5dc75bd6d8239892302
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 11:06:44 2023 +0800

    优化手机端logo文字显示问题

commit 018dabb2faddc0541fb33ef5a51a126575a59cf5
Author: Sun <95302870@qq.com>
Date:   Mon Nov 27 10:51:02 2023 +0800

    更新说明文件

commit 02239e3686933e4c33e430be8300ec2d0be41887
Author: Sun <95302870@qq.com>
Date:   Sun Nov 26 22:59:11 2023 +0800

    优化 登录页面

commit 6aa92e8ba6c4eeb2fa2995ee93aeeac86b54b551
Author: Sun <95302870@qq.com>
Date:   Sat Nov 25 23:59:40 2023 +0800

    增加编译脚本

commit d93df810fa95a7baa28ca5323903b93f286ba741
Author: Sun <95302870@qq.com>
Date:   Sat Nov 25 15:48:00 2023 +0800

    修改相关logo图片

commit 036a56ddc7a555d6227c92dfa2abfe84f9042662
Merge: 7018872 feacc89
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 16:00:23 2023 +0800

    Merge branch 'master' into dev

commit 7018872ce9fd0fa8f1ff4731a16b2ea90fb9153f
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 15:31:31 2023 +0800

    更新版本标签

commit 4fae97dd932ce4638d869a0c7a123c788c3e3e43
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 15:07:39 2023 +0800

    更新版本1.1.0 测试版

commit 890a3c3dbdccbe4dfd5a6915e87a2649c9141e7b
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 14:31:26 2023 +0800

    右键菜单新增打开局域网或者互联网地址,优化分组管理图标不统一的问题

commit 4f014cf4aa384a2c8a03585ffd6ce41c941b9356
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 13:33:43 2023 +0800

    增加 关联删除,优化添加密码长度限制20

commit 5658e6c379b077d359fff75c5e9b904cbce5f81e
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 12:09:41 2023 +0800

    增加更新日志

commit f142d1b378e0525db157a93cca61ee86bf1eb08d
Author: Sun <95302870@qq.com>
Date:   Fri Nov 24 12:09:30 2023 +0800

    添加应用图标验证分组信息必填

commit 2ff2b6b32a4bb70653e3a7312ccb0f4b0b945f07
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 23:45:10 2023 +0800

    优化关于页面,及更新版本序号为2

commit c9b482b24e2d23d638501dbaa44f826386c420b5
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 22:12:13 2023 +0800

    优化关于设置版本号

commit ed70059ffbce1ae8a9e2e0378803f7875ada342b
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 21:41:22 2023 +0800

    修复分组管理不能滚动的问题

commit faa4222b1494271878c2c7469c14a4efa49c6761
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 21:24:34 2023 +0800

    修复分组写死的问题

commit 4f2d0c858e55735b9ba3a8453de9407b76346805
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 20:24:39 2023 +0800

    初步尝试构建测试版本

commit 596bed19dcf3bceb77e30f9c24218888f8da7e64
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 19:24:05 2023 +0800

    修复搜索框配置bug,云端没有默认值,前端打不开搜索引擎选择栏

commit 489fbf748a7e35c6b69198b19038c01a548e20f2
Author: Sun <95302870@qq.com>
Date:   Thu Nov 23 19:22:57 2023 +0800

    增加logo和版本打印,修复模块配置的索引报错

commit 263dab607af8a830acee44e37776bda4da814b40
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 23:02:37 2023 +0800

    调整排序样式

commit c0adf335d3e48e6770d56eb506b471852fbfbc43
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 22:58:18 2023 +0800

    说明文件增加logo

commit 721d22e75b93d3646f5f206d208a9a743840a25b
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 22:40:40 2023 +0800

    更换logo

commit 4df58fec7b2054ce97cf2989045affd144aa4f8b
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 22:38:27 2023 +0800

    完善关于页面

commit 63777f0bbac85550fafe1b084bd664c7722ab934
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 21:12:14 2023 +0800

    字体为纯白色的时候,详情图标会根据背景的明暗度计算字体颜色

commit f328dc73305665a921e030dd4a06d759d0cac3bf
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 17:21:40 2023 +0800

    详情图标居中

commit 663f37bf1a26b7dff24edcfb149222ac780cb90d
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 16:45:11 2023 +0800

    将图标单独拆分为子组件应用图标

commit 30cd5ab460e032f7f6d7c23eb7a9c7af735d0f41
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 13:36:32 2023 +0800

    增加详情图标隐藏描述信息等设置

commit 945a94e76cae4251953512cc09f348ad38bb9a38
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 12:28:46 2023 +0800

    优化图标背景色:支持透明图标并更换背景颜色字段

commit 437053fc9d8d9e3c55aac4d259e4b4c4bc11de58
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 11:20:56 2023 +0800

    完善搜索框

commit a9914f8e8ced23b8c50701a85d522c8f0fcd1c2b
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 01:27:16 2023 +0800

    关闭模块配置相关接口开发模式

commit 2a9e22d4b781f43c0f9b8a867d26c295e756175b
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 01:24:09 2023 +0800

    完成搜索框的样式和模块配置的state等api对接

commit 7f771650ef7272e474f74ed689ab844bf90b946f
Author: Sun <95302870@qq.com>
Date:   Wed Nov 22 00:45:25 2023 +0800

    增加搜索引擎图标

commit a0e0039ae89eaa27e4b849baa0168716866682ea
Author: Sun <95302870@qq.com>
Date:   Tue Nov 21 19:54:36 2023 +0800

    增加 模块配置表

commit 017869794177d7a5d4c12c0eac6fb7c7fe79734e
Author: Sun <95302870@qq.com>
Date:   Tue Nov 21 13:10:39 2023 +0800

    图标标题加粗

commit 7a2d896a44262b54d6a1d2d12fe6bbbc31b1ca49
Author: Sun <95302870@qq.com>
Date:   Tue Nov 21 12:53:39 2023 +0800

    增加图标的预设颜色

commit a6c3120c186646b323e57ecce0f85ec9c79a41a5
Author: Sun <95302870@qq.com>
Date:   Tue Nov 21 12:18:46 2023 +0800

    增加遮罩

commit 84d3db81ea2aaa0f67a67690e449d15401e8e511
Author: Sun <95302870@qq.com>
Date:   Tue Nov 21 11:05:24 2023 +0800

    极简小图标增加鼠标悬浮详情

commit 666a6a117bc30c64a78ab0fe2cb7836c602b2741
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 23:33:10 2023 +0800

    修复 sort字段未修改归0的问题

commit 71afd530d7a740763326a6117f8e7c04ac1f7f69
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 23:32:48 2023 +0800

    适配纯透明图标,增强图标背景色,增加图标url连接支持

commit 619c5e28e1c51c16e14ed09601709ab751ee2454
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 22:37:43 2023 +0800

    修复 每次修改图标都重置了排序号

commit 8a17f1c0bf2f00c530dee61cd5070a74ca4a53b2
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 21:21:20 2023 +0800

    分组为空的时候显示添加图标的图标

commit 755cf3dc569e402cb3dfc9915e94de4f2595571b
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 20:52:45 2023 +0800

    首页图标排序完成

commit 5ccf23c68b3284be00dadf94073a665826737a77
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 14:30:12 2023 +0800

    添加修改图标适配分组

commit 47209d729270bf4704428ecd91606d4721bd9a13
Author: Sun <95302870@qq.com>
Date:   Mon Nov 20 11:06:32 2023 +0800

    保存分组和分组排序已经完成

commit 17403de7ed236a097d70cd5ecbbe261e620ff377
Merge: d0d88eb 980d81a
Author: Sun <95302870@qq.com>
Date:   Sun Nov 19 23:38:00 2023 +0800

    Merge branch 'master' into dev

commit d0d88eb548bbe9d7f5ad663f383db858843a8d8c
Merge: 728dbc8 47b479c
Author: Sun <95302870@qq.com>
Date:   Sun Nov 19 11:13:34 2023 +0800

    Merge branch 'docker-build' into dev

commit 47b479cf8da7214dd9e0592b461743ab7d3824ed
Author: Sun <95302870@qq.com>
Date:   Sun Nov 19 11:12:54 2023 +0800

    修改前端程序名

commit 728dbc80ff7885d0b4cf289b06763cc60ed17d7e
Author: Sun <95302870@qq.com>
Date:   Thu Nov 16 13:44:47 2023 +0800

    新增删除应用分组和修改应用分组,以及图标真正的按组读取

commit a3dbd948ca743384a2de3685083695603d674bf1
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 22:49:58 2023 +0800

    增加图标组api

commit de21f3f232c1243917b5c55ba4bedb01437f8564
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 22:49:44 2023 +0800

    重新划分应用盒子的结构

commit 7c409112ba1f8eefb7df7fffdb78b285e3f5322c
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 22:27:07 2023 +0800

    [后端] 增加应用分组

commit ebf9500529c7db30b1c6e1ed4056013d0f83827a
Merge: acedcb3 97d4f83
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 20:38:05 2023 +0800

    Merge branch 'feature/drag' into dev

commit acedcb32a03ed0ee1833143912a9215182da3fb6
Merge: f105e10 c84eae3
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 20:37:26 2023 +0800

    Merge branch 'master' into dev

commit 97d4f8368dffca2a16d729e666068a552feca87d
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 20:36:19 2023 +0800

    更新软件包

commit 5108f65275181b899b8fc100c615cb6065dcca5d
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 20:30:39 2023 +0800

    简单监听了一下拖拽

commit dae9aea41f1540ccb74abea2a31af5d2a1e4dcfd
Merge: 396db51 f672034
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 10:01:00 2023 +0800

    Merge branch 'master' into feature/drag

commit 396db51979d513559512b0a9702dd0d616c2872b
Author: Sun <95302870@qq.com>
Date:   Wed Nov 15 00:08:02 2023 +0800

    历史性时刻,拖拽图标

commit f105e10fe1ced11d0b32eba37cfbfdb94f6ad07b
Author: Sun <95302870@qq.com>
Date:   Tue Nov 14 11:35:52 2023 +0800

    尝试增加一个分组标题

commit 7e2354f4ed509c7d05667604b7eb56e91f911ed0
Author: Sun <95302870@qq.com>
Date:   Sun Nov 12 23:07:37 2023 +0800

    优化 枚举引用错误

commit 27e85b7da339706ea97604a785bf013dad5f9534
Author: Sun <95302870@qq.com>
Date:   Sun Nov 12 23:06:50 2023 +0800

    优化路由

commit fef462804c0d445f5b9bc7e38e226b55c26017ee
Author: Sun <95302870@qq.com>
Date:   Sun Nov 12 21:28:57 2023 +0800

    更换enums的位置
This commit is contained in:
Sun 2023-11-29 10:23:57 +08:00
parent b057e25443
commit 9cf463b04c
83 changed files with 2089 additions and 580 deletions

View File

@ -16,12 +16,13 @@ COPY . /build
RUN pnpm run build
# build backend
FROM golang:1.19 as server_image
FROM golang:1.21-alpine as server_image
WORKDIR /build
COPY ./service .
RUN apk add --no-cache bash curl gcc git go musl-dev
# 执行指令 关闭链接确认
RUN go env -w GO111MODULE=on \
@ -34,7 +35,7 @@ RUN go env -w GO111MODULE=on \
# run_image
FROM ubuntu
FROM alpine
WORKDIR /app
@ -42,6 +43,8 @@ COPY --from=web_image /build/dist /app/web
COPY --from=server_image /build/sun-panel /app/sun-panel
RUN apt-get update && apt-get install -y ca-certificates &&./sun-panel -config
RUN apk add --no-cache bash ca-certificates su-exec tzdata \
&& chmod +x ./sun-panel \
&& ./sun-panel -config
CMD ./sun-panel

View File

@ -1,5 +1,7 @@
<div align=center>
<img src="./doc/images/logo.png" width="100" height="100" />
# Sun-Panel
<a href="https://github.com/hslr-s/sun-panel.git">Github</a> | <a href="https://gitee.com/hslr/sun-panel.git">Gitee</a> | <a href="https://hub.docker.com/r/hslr/sun-panel">Docker Hub</a> | <a href="https://www.bilibili.com/video/BV1AC4y1U7va">B站视频</a>
@ -8,13 +10,13 @@
</div>
![](./doc/images/icon-info.jpg)
![](./doc/images/icon-info-new.png)
## 😎 特点
- 局域网内外网链接切换
- 简洁
- docker 部署
- 局域网内外网链接切换
- docker部署,对arm系统支持
- 上手简单,免修改代码
- 无需连接外部数据库
- 丰富图标自由搭配(文字图标+svg图标+内置三方图标库)
@ -47,22 +49,36 @@
先画个饼
- [ ] 图标排序
- [x] 分组,拖拽排序
- [ ] 导入导出功能
- [ ] 增加访客账号
- [ ] 用户自定义搜索框搜索引擎
- [ ] 搜索框样式自定义(背景颜色,文字颜色)
- [ ] 帐号解除邮箱限制
- [ ] 对上传的文件管理(针对账户增强重复利用,节省空间)
- [ ] 多国语言支持
- [ ] 服务器监控
- [ ] docker管理器
- [ ] 计划任务
## 🖼️ 预览截图
![](./doc/images/icon-small.jpg)
![](./doc/images/full-color-info.jpg)
**各种风格,自由搭配**
![](./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>
@ -78,7 +94,20 @@
|-config|生成配置文件conf/conf.ini|
|-password-reset|重置第一个用户的密码|
### 二进制文件运行
去 [Releases](https://github.com/hslr-s/sun-panel/releases) 下载二进制文件
执行示例
```sh
./sun-panel
```
#### 重置密码
执行示例
```sh
./sun-panel -password-reset
```
@ -115,14 +144,14 @@ hslr/sun-panel
```
### 编译运行
### 编译运行
#### 前端
```
# 开发运行
pnpm dev
# 编译打包
# 编译打包(打包后生成dist目录若需要结合后端使用请改成web)
pnpm build
```

22
UPDATELOG.md Normal file
View File

@ -0,0 +1,22 @@
# 更新说明
> 老用户版本升级需要看升级说明,并且一定提前备份好重要数据。新用户可以直接使用最新版本。
## v1.1.0
> 支持上个版本直接升级无需特殊处理
- [新增] 增加分组,拖拽排序
- [新增] 搜索框
- [新增] 应用图标支持URL外链
- [新增] 图标支持纯透明
- [新增] 壁纸背景增加遮罩设置
- [新增] 右键菜单新增打开局域网或者互联网地址
- [优化] 网址输入框增加https/http提示
- [优化] 小图标模式,鼠标悬浮显示详情,支持隐藏图标标题
- [优化] 详情图标样式,支持隐藏描述信息
- [优化] 添加用户密码时限制字符
- [其他] 新增arm版本docker支持。[DockerHub](https://hub.docker.com/r/hslr/sun-panel)直接拉取即可
- [其他] 新增多平台二进制文件运行。[Releases](https://github.com/hslr-s/sun-panel/releases)
## v1.0.0
- 首个版本

143
build.sh Normal file
View File

@ -0,0 +1,143 @@
#!/bin/bash
REPO=$(
cd $(dirname $0)
pwd
)
COMMIT_SHA=$(git rev-parse --short HEAD)
VERSION=$(git describe --tags)
# VERSION="0.1.1"
FRONTEND="false"
BINARY="false"
RELEASE="false"
debugInfo() {
echo "Repo: $REPO"
echo "Build frontend: $FRONTEND"
echo "Build binary: $BINARY"
echo "Release: $RELEASE"
echo "Version: $VERSION"
echo "Commit: $COMMIT_SHA"
}
buildFrontend() {
cd $REPO
pwd
# npm install pnpm -g
pnpm install
pnpm run build
}
buildBackEndAssets() {
cd $REPO/service
# export PATH=$PATH:/root/go/bin
go install -a -v github.com/go-bindata/go-bindata/...@latest
go install -a -v github.com/elazarl/go-bindata-assetfs/...@latest
go-bindata-assetfs -o=assets/bindata.go -pkg=assets assets/...
}
# buildBinary() {
# cd $REPO/service
# # mv "${REPO}/dist" "${REPO}/web"
# go build -o "sun-panel" --ldflags="-X sun-panel/global.RUNCODE=release" main.go
# }
_build() {
cd $REPO/service
pwd
local osarch=$1
IFS=/ read -r -a arr <<<"$osarch"
os="${arr[0]}"
arch="${arr[1]}"
gcc="${arr[2]}"
# Go build to build the binary.
export GOOS=$os
export GOARCH=$arch
export CC=$gcc
export CGO_ENABLED=1
pathRelease=$REPO/release
if [ -n "$VERSION" ]; then
outPath="sun-panel_${VERSION}_${os}_${arch}"
else
outPath="sun-panel_${COMMIT_SHA}_${os}_${arch}"
fi
outname="${pathRelease}/${outPath}/sun-panel"
go build -o "${outname}" --ldflags="-X sun-panel/global.RUNCODE=release" main.go
cd $pathRelease
# copy front file
cp -r "${REPO}/dist" "${pathRelease}/${outPath}/web"
echo "Release ${outPath}"
if [ "$os" = "windows" ]; then
mv $outname $outPath/sun-panel.exe
zip -r "${pathRelease}/${outPath}.zip" $outPath
else
mv $outname $outPath/sun-panel
tar -zcvf "${pathRelease}/${outPath}.tar.gz" $outPath
fi
rm -rf "${pathRelease}/${outPath}"
}
release() {
cd $REPO/service
## List of architectures and OS to test coss compilation.
SUPPORTED_OSARCH="linux/amd64/gcc linux/arm/arm-linux-gnueabihf-gcc windows/amd64/x86_64-w64-mingw32-gcc linux/arm64/aarch64-linux-gnu-gcc"
echo "Release builds for OS/Arch/CC: ${SUPPORTED_OSARCH}"
for each_osarch in ${SUPPORTED_OSARCH}; do
_build "${each_osarch}"
done
}
usage() {
# echo "Usage: $0 [-f] [-c] [-b] [-r]" 1>&2
echo "Usage: $0 [-f] [-b] [-r]" 1>&2
exit 1
}
while getopts "bfcrd" o; do
case "${o}" in
b)
FRONTEND="true"
BINARY="true"
;;
f)
FRONTEND="true"
;;
c)
BINARY="true"
;;
r)
FRONTEND="true"
RELEASE="true"
;;
d)
DEBUG="true"
;;
*)
usage
;;
esac
done
shift $((OPTIND - 1))
if [ "$DEBUG" = "true" ]; then
debugInfo
fi
if [ "$FRONTEND" = "true" ]; then
buildFrontend
fi
# if [ "$BINARY" = "true" ]; then
# buildBinary
# fi
if [ "$RELEASE" = "true" ]; then
buildBackEndAssets
release
fi

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

BIN
doc/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
version: "3.2"
services:
sun-panel:
image: 'hslr/sun-panel:latest'
container_name: sun-panel
volumes:
- ./conf:/app/conf
- ./uploads:/app/uploads
- ./database:/app/database
# - ./runtime:/app/runtime
ports:
- 3002:3002
restart: always

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "chatgpt-web",
"name": "sun-panel",
"version": "2.10.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "chatgpt-web",
"name": "sun-panel",
"version": "2.10.9",
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",

View File

@ -1,11 +1,11 @@
{
"name": "chatgpt-web",
"name": "sun-panel",
"version": "2.10.9",
"private": false,
"description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"Sun-Panel",
"chatgpt",
"chatbot",
"vue"
@ -32,6 +32,7 @@
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"vue": "^3.2.47",
"vue-draggable-plus": "^0.2.6",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vuedraggable": "^4.1.0"

74
pnpm-lock.yaml generated
View File

@ -11,15 +11,9 @@ dependencies:
'@vueuse/core':
specifier: ^9.13.0
version: 9.13.0(vue@3.2.47)
echarts:
specifier: ^5.4.2
version: 5.4.2
highlight.js:
specifier: ^11.7.0
version: 11.7.0
html2canvas:
specifier: ^1.4.1
version: 1.4.1
katex:
specifier: ^0.16.4
version: 0.16.4
@ -38,6 +32,9 @@ dependencies:
vue:
specifier: ^3.2.47
version: 3.2.47
vue-draggable-plus:
specifier: ^0.2.6
version: 0.2.6(@types/sortablejs@1.15.5)
vue-i18n:
specifier: ^9.2.2
version: 9.2.2(vue@3.2.47)
@ -2174,6 +2171,10 @@ packages:
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
dev: true
/@types/sortablejs@1.15.5:
resolution: {integrity: sha512-qqqbEFbB1EZt08I1Ok2BA3Sx0zlI8oizdIguMsajk4Yo/iHgXhCb3GM6N09JOJqT9xIMYM9LTFy8vit3RNY71Q==}
dev: false
/@types/trusted-types@2.0.3:
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
dev: true
@ -2733,11 +2734,6 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
dev: false
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
@ -3087,12 +3083,6 @@ packages:
engines: {node: '>=8'}
dev: true
/css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
dependencies:
utrie: 1.0.2
dev: false
/css-render@0.15.12:
resolution: {integrity: sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==}
dependencies:
@ -3279,13 +3269,6 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/echarts@5.4.2:
resolution: {integrity: sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==}
dependencies:
tslib: 2.3.0
zrender: 5.4.3
dev: false
/ejs@3.1.8:
resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==}
engines: {node: '>=0.10.0'}
@ -4238,14 +4221,6 @@ packages:
lru-cache: 6.0.0
dev: true
/html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
dev: false
/htmlparser2@8.0.1:
resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
dependencies:
@ -5093,6 +5068,7 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
requiresBuild: true
dev: true
/muggle-string@0.2.2:
@ -6224,12 +6200,6 @@ packages:
engines: {node: '>=0.10'}
dev: true
/text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
dependencies:
utrie: 1.0.2
dev: false
/text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
@ -6314,10 +6284,6 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
/tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
dev: false
/tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
dev: true
@ -6462,12 +6428,6 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
dependencies:
base64-arraybuffer: 1.0.2
dev: false
/v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true
@ -6566,6 +6526,18 @@ packages:
vue: 3.2.47
dev: false
/vue-draggable-plus@0.2.6(@types/sortablejs@1.15.5):
resolution: {integrity: sha512-d+0omKIBIfLiJFggc6H4ePRaifbX+33+OiCMsxn8rG59yWXlJGrobexxgXetnSo/1NLTd0TkYZKNc4CA6iwJZw==}
peerDependencies:
'@types/sortablejs': ^1.15.0
'@vue/composition-api': '*'
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dependencies:
'@types/sortablejs': 1.15.5
dev: false
/vue-eslint-parser@9.1.0(eslint@8.35.0):
resolution: {integrity: sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==}
engines: {node: ^14.17.0 || >=16.0.0}
@ -6953,9 +6925,3 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
/zrender@5.4.3:
resolution: {integrity: sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==}
dependencies:
tslib: 2.3.0
dev: false

1
public/favicon-black.svg Normal file
View File

@ -0,0 +1 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 566.93 566.93"><defs><style>.cls-1{fill:#040000;}</style></defs><path class="cls-1" d="M99.37,502.4H345.56A156.16,156.16,0,0,0,501.72,346.25h0A156.16,156.16,0,0,0,345.56,190.1h-123A33.54,33.54,0,0,0,189,223.61v1a33.54,33.54,0,0,0,33.54,33.56H344.44c48.51,0,88.77,38.74,89.24,87.25a88.22,88.22,0,0,1-88.12,89s-163.09.37-245.94-.27a34.15,34.15,0,0,0-34.41,34.15h0A34.15,34.15,0,0,0,99.37,502.4Z"/><path class="cls-1" d="M467.56,64.53H221.37A156.15,156.15,0,0,0,65.21,220.68h0A156.15,156.15,0,0,0,221.37,376.83h123a33.54,33.54,0,0,0,33.54-33.51v-1a33.54,33.54,0,0,0-33.54-33.56H222.49c-48.52,0-88.77-38.74-89.24-87.25a88.22,88.22,0,0,1,88.12-89s163.09-.37,245.94.27a34.15,34.15,0,0,0,34.41-34.15h0A34.15,34.15,0,0,0,467.56,64.53Z"/></svg>

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M663 1649 c-131 -41 -254 -157 -304 -286 -33 -86 -38 -221 -11 -310 36 -117 97 -200 196 -265 108 -70 153 -78 438 -78 l250 0 29 29 c38 39 40 97 5 138 l-24 28 -239 5 c-270 7 -300 13 -370 74 -149 131 -116 368 62 452 l50 24 463 0 462 0 0 105 0 105 -472 -1 c-442 0 -477 -1 -535 -20z"/><path d="M739 1261 c-38 -39 -40 -97 -5 -138 l24 -28 239 -5 c270 -7 300 -13 370 -74 150 -132 115 -373 -67 -455 -43 -20 -64 -21 -507 -21 l-463 0 0 -105 0 -105 469 0 c443 0 472 1 535 20 133 42 256 156 307 287 33 86 38 221 11 310 -23 74 -81 171 -128 214 -49 45 -126 89 -193 109 -56 18 -93 20 -313 20 l-250 0 -29 -29z"/></g></svg>
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 566.93 566.93"><defs><style>.cls-1{fill:#4fb3bb;}</style></defs><path class="cls-1" d="M99.37,502.4H345.56A156.16,156.16,0,0,0,501.72,346.25h0A156.16,156.16,0,0,0,345.56,190.1h-123A33.54,33.54,0,0,0,189,223.61v1a33.54,33.54,0,0,0,33.54,33.56H344.44c48.51,0,88.77,38.74,89.24,87.25a88.22,88.22,0,0,1-88.12,89s-163.09.37-245.94-.27a34.15,34.15,0,0,0-34.41,34.15h0A34.15,34.15,0,0,0,99.37,502.4Z"/><path class="cls-1" d="M467.56,64.53H221.37A156.15,156.15,0,0,0,65.21,220.68h0A156.15,156.15,0,0,0,221.37,376.83h123a33.54,33.54,0,0,0,33.54-33.51v-1a33.54,33.54,0,0,0-33.54-33.56H222.49c-48.52,0-88.77-38.74-89.24-87.25a88.22,88.22,0,0,1,88.12-89s163.09-.37,245.94.27a34.15,34.15,0,0,0,34.41-34.15h0A34.15,34.15,0,0,0,467.56,64.53Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 823 B

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -20,3 +20,12 @@ type VerificationResponse struct {
Result bool `json:"result"`
Message string `json:"message"`
}
type SortRequestItem struct {
Id uint `json:"id"`
Sort uint `json:"sort"`
}
type SortRequest struct {
SortItems []SortRequestItem `json:"sortItems"`
}

View File

@ -1,8 +1,16 @@
package adminApiStructs
package panelApiStructs
import "sun-panel/models"
import (
"sun-panel/api/api_v1/common/apiData/commonApiStructs"
"sun-panel/models"
)
type ItemIconEditRequest struct {
models.ItemIcon
IconJson string
}
type ItemIconSaveSortRequest struct {
SortItems []commonApiStructs.SortRequestItem `json:"sortItems"`
ItemIconGroupId uint `json:"itemIconGroupId"`
}

View File

@ -1,7 +1,8 @@
package panel
type ApiPanel struct {
ItemIcon ItemIcon
UserConfig UserConfig
UsersApi UsersApi
ItemIcon ItemIcon
UserConfig UserConfig
UsersApi UsersApi
ItemIconGroup ItemIconGroup
}

View File

@ -0,0 +1,152 @@
package panel
import (
"math"
"sun-panel/api/api_v1/common/apiData/commonApiStructs"
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/api/api_v1/common/base"
"sun-panel/global"
"sun-panel/models"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gorm.io/gorm"
)
type ItemIconGroup struct {
}
func (a *ItemIconGroup) Edit(c *gin.Context) {
userInfo, _ := base.GetCurrentUserInfo(c)
req := models.ItemIconGroup{}
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
req.UserId = userInfo.ID
if req.ID != 0 {
// 修改
updateField := []string{"IconJson", "Icon", "Title", "Url", "LanUrl", "Description", "OpenMethod", "GroupId", "UserId"}
if req.Sort != 0 {
updateField = append(updateField, "Sort")
}
global.Db.Model(&models.ItemIconGroup{}).
Select(updateField).
Where("id=?", req.ID).Updates(&req)
} else {
// 创建
global.Db.Create(&req)
}
apiReturn.SuccessData(c, req)
}
func (a *ItemIconGroup) GetList(c *gin.Context) {
userInfo, _ := base.GetCurrentUserInfo(c)
groups := []models.ItemIconGroup{}
if err := global.Db.Order("sort ,created_at").Where("user_id=?", userInfo.ID).Find(&groups).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
// 判断分组是否为空,为空将自动创建默认分组
if len(groups) == 0 {
defaultGroup := models.ItemIconGroup{
Title: "APP",
UserId: userInfo.ID,
Icon: "material-symbols:ad-group-outline"}
if err := global.Db.Create(&defaultGroup).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
// 并将当前账号下所有无分组的图标更新到当前组
if err := global.Db.Model(&models.ItemIcon{}).Where("user_id=?", userInfo.ID).Update("item_icon_group_id", defaultGroup.ID).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
groups = append(groups, defaultGroup)
}
apiReturn.SuccessListData(c, groups, 0)
}
func (a *ItemIconGroup) Deletes(c *gin.Context) {
req := commonApiStructs.RequestDeleteIds[uint]{}
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
userInfo, _ := base.GetCurrentUserInfo(c)
var count int64
if err := global.Db.Model(&models.ItemIconGroup{}).Where(" user_id=?", userInfo.ID).Count(&count).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
} else {
if math.Abs(float64(len(req.Ids))-float64(count)) < 1 {
apiReturn.Error(c, "至少要保留一个")
return
}
}
txErr := global.Db.Transaction(func(tx *gorm.DB) error {
mitemIcon := models.ItemIcon{}
if err := tx.Delete(&models.ItemIconGroup{}, "id in ? AND user_id=?", req.Ids, userInfo.ID).Error; err != nil {
return err
}
if err := mitemIcon.DeleteByItemIconGroupIds(tx, userInfo.ID, req.Ids); err != nil {
return err
}
return nil
})
if txErr != nil {
apiReturn.ErrorDatabase(c, txErr.Error())
return
}
apiReturn.Success(c)
}
// 保存排序
func (a *ItemIconGroup) SaveSort(c *gin.Context) {
req := commonApiStructs.SortRequest{}
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
userInfo, _ := base.GetCurrentUserInfo(c)
transactionErr := global.Db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db'
for _, v := range req.SortItems {
if err := tx.Model(&models.ItemIconGroup{}).Where("user_id=? AND id=?", userInfo.ID, v.Id).Update("sort", v.Sort).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
}
// 返回 nil 提交事务
return nil
})
if transactionErr != nil {
apiReturn.ErrorDatabase(c, transactionErr.Error())
return
}
apiReturn.Success(c)
}

View File

@ -3,6 +3,7 @@ package panel
import (
"encoding/json"
"sun-panel/api/api_v1/common/apiData/commonApiStructs"
"sun-panel/api/api_v1/common/apiData/panelApiStructs"
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/api/api_v1/common/base"
"sun-panel/global"
@ -10,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gorm.io/gorm"
)
type ItemIcon struct {
@ -24,9 +26,12 @@ func (a *ItemIcon) Edit(c *gin.Context) {
return
}
if req.ItemIconGroupId == 0 {
apiReturn.Error(c, "分组为必填项")
return
}
req.UserId = userInfo.ID
req.GroupId = 1
req.Sort = 1
// json转字符串
if j, err := json.Marshal(req.Icon); err == nil {
@ -35,10 +40,15 @@ func (a *ItemIcon) Edit(c *gin.Context) {
if req.ID != 0 {
// 修改
updateField := []string{"IconJson", "Icon", "Title", "Url", "LanUrl", "Description", "OpenMethod", "GroupId", "UserId", "ItemIconGroupId"}
if req.Sort != 0 {
updateField = append(updateField, "Sort")
}
global.Db.Model(&models.ItemIcon{}).
Select("IconJson", "Icon", "Title", "Url", "LanUrl", "Description", "OpenMethod", "Sort", "GroupId", "UserId").
Select(updateField).
Where("id=?", req.ID).Updates(&req)
} else {
req.Sort = 9999
// 创建
global.Db.Create(&req)
}
@ -77,7 +87,7 @@ func (a *ItemIcon) Edit(c *gin.Context) {
// }
func (a *ItemIcon) GetListByGroupId(c *gin.Context) {
req := commonApiStructs.RequestPage{}
req := models.ItemIcon{}
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
@ -87,7 +97,7 @@ func (a *ItemIcon) GetListByGroupId(c *gin.Context) {
userInfo, _ := base.GetCurrentUserInfo(c)
itemIcons := []models.ItemIcon{}
if err := global.Db.Order("sort ,created_at DESC").Where("user_id=?", userInfo.ID).Find(&itemIcons, "group_id = ? AND user_id=?", 1, userInfo.ID).Error; err != nil {
if err := global.Db.Order("sort ,created_at").Find(&itemIcons, "item_icon_group_id = ? AND user_id=?", req.ItemIconGroupId, userInfo.ID).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
@ -108,10 +118,42 @@ func (a *ItemIcon) Deletes(c *gin.Context) {
}
userInfo, _ := base.GetCurrentUserInfo(c)
if err := global.Db.Debug().Delete(&models.ItemIcon{}, "id in ? AND user_id=?", req.Ids, userInfo.ID).Error; err != nil {
if err := global.Db.Delete(&models.ItemIcon{}, "id in ? AND user_id=?", req.Ids, userInfo.ID).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
apiReturn.Success(c)
}
// 保存排序
func (a *ItemIcon) SaveSort(c *gin.Context) {
req := panelApiStructs.ItemIconSaveSortRequest{}
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
userInfo, _ := base.GetCurrentUserInfo(c)
transactionErr := global.Db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db'
for _, v := range req.SortItems {
if err := tx.Model(&models.ItemIcon{}).Where("user_id=? AND id=? AND item_icon_group_id=?", userInfo.ID, v.Id, req.ItemIconGroupId).Update("sort", v.Sort).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
}
// 返回 nil 提交事务
return nil
})
if transactionErr != nil {
apiReturn.ErrorDatabase(c, transactionErr.Error())
return
}
apiReturn.Success(c)
}

View File

@ -1,6 +1,7 @@
package panel
import (
"math"
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/api/api_v1/common/base"
"sun-panel/global"
@ -9,6 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gorm.io/gorm"
)
// 此API 临时使用,后期带有管理功能,将废除!!!
@ -64,10 +66,55 @@ func (a UsersApi) Deletes(c *gin.Context) {
return
}
if err := global.Db.Delete(&models.User{}, &param.UserIds).Error; err != nil {
var count int64
if err := global.Db.Model(&models.User{}).Count(&count).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
} else {
if math.Abs(float64(len(param.UserIds))-float64(count)) < 1 {
apiReturn.Error(c, "至少要保留一个")
return
}
}
txErr := global.Db.Transaction(func(tx *gorm.DB) error {
mitemIconGroup := models.ItemIconGroup{}
for _, v := range param.UserIds {
// 删除图标
if err := tx.Delete(&models.ItemIcon{}, "user_id=?", v).Error; err != nil {
return err
}
// 删除分组
if err := mitemIconGroup.DeleteByUserId(tx, v); err != nil {
return err
}
// 删除模块配置
if err := tx.Delete(&models.ModuleConfig{}, "user_id=?", v).Error; err != nil {
return err
}
// 删除用户配置
if err := tx.Delete(&models.ModuleConfig{}, "user_id=?", v).Error; err != nil {
return err
}
// // 删除文件记录(不删除资源文件)
// if err := tx.Delete(&models.File{}, "user_id=?", v).Error; err != nil {
// return err
// }
}
if err := tx.Delete(&models.User{}, &param.UserIds).Error; err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return err
}
return nil
})
if txErr != nil {
apiReturn.ErrorDatabase(c, txErr.Error())
return
}
apiReturn.Success(c)
}

View File

@ -1,11 +1,12 @@
package system
type ApiSystem struct {
About About
LoginApi LoginApi
UserApi UserApi
FileApi FileApi
CaptchaApi CaptchaApi
RegisterApi RegisterApi
NoticeApi NoticeApi
About About
LoginApi LoginApi
UserApi UserApi
FileApi FileApi
CaptchaApi CaptchaApi
RegisterApi RegisterApi
NoticeApi NoticeApi
ModuleConfigApi ModuleConfigApi
}

View File

@ -0,0 +1,53 @@
package system
import (
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/api/api_v1/common/base"
"sun-panel/global"
"sun-panel/models"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type ModuleConfigApi struct{}
func (a *ModuleConfigApi) GetByName(c *gin.Context) {
req := models.ModuleConfig{}
if err := c.ShouldBindWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
userInfo, _ := base.GetCurrentUserInfo(c)
mCfg := models.ModuleConfig{}
if cfg, err := mCfg.GetConfigByUserIdAndName(global.Db, userInfo.ID, req.Name); err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
} else {
apiReturn.SuccessData(c, cfg)
return
}
}
func (a *ModuleConfigApi) Save(c *gin.Context) {
req := models.ModuleConfig{}
if err := c.ShouldBindWith(&req, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
userInfo, _ := base.GetCurrentUserInfo(c)
mCfg := models.ModuleConfig{}
mCfg.UserId = userInfo.ID
mCfg.Value = req.Value
mCfg.Name = req.Name
if err := mCfg.Save(global.Db); err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
apiReturn.Success(c)
}

View File

@ -23,8 +23,8 @@ type LoginApi struct {
// 登录输入验证
type LoginLoginVerify struct {
Username string `json:"username" validate:"required,min=5"`
Password string `json:"password" validate:"required,min=5,max=20"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required,max=50"`
VCode string `json:"vcode" validate:"max=6"`
Email string `json:"email"`
}

View File

@ -23,7 +23,7 @@ host=127.0.0.1
port=3306
username=root
password=root
db_name=chatgpt
db_name=sun_panel
wait_timeout=100
# ======================
@ -39,37 +39,4 @@ file_path=./database/database.db
address=127.0.0.1:6379
password=
prefix=sun_panel:
db=0
# ================
# proxy
# ================
[proxy]
url=
# ======================
# Automatically generated
# Prohibit modification
# ======================
[build]
conf_version=1
; install_time=132456
# ======================
# Initial configuration
# ======================
# Automatically delete after successful initialization
[init]
# Administrator account (try to use email format; 6-16 digits)
admin_username=admin
# Administrator password (6-16 digits)
admin_password=123456
# ======================
# 支付宝支付相关配置
# ======================
[alipay]
appid=
private_key=
alipay_public_key=
db=0

View File

@ -1 +1 @@
1|1.0.0
3|1.1.0

View File

@ -29,6 +29,7 @@ var DB_DRIVER = database.SQLITE
// var ISDOCER = "" // 是否为docker模式
func InitApp() error {
Logo()
gin.SetMode(global.RUNCODE) // GIN 运行模式
// 日志
@ -176,3 +177,17 @@ func CommandRun() {
}
os.Exit(0) // 务必退出
}
func Logo() {
fmt.Println(" ____ ___ __")
fmt.Println(" / __/_ _____ / _ \\___ ____ ___ / /")
fmt.Println(" _\\ \\/ // / _ \\ / ___/ _ `/ _ \\/ -_) / ")
fmt.Println(" /___/\\_,_/_//_/ /_/ \\_,_/_//_/\\__/_/ ")
fmt.Println("")
versionInfo := cmn.GetSysVersionInfo()
fmt.Println("Version:", versionInfo.Version)
fmt.Println("Welcome to the Sun-Panel.")
fmt.Println("Project address:", "https://github.com/hslr-s/sun-panel")
}

View File

@ -76,7 +76,7 @@ func (d *SQLiteConfig) Connect() (db *gorm.DB, err error) {
// 创建文件夹
if !exists {
if err = os.MkdirAll(path.Dir(filePath), 0666); err != nil {
if err = os.MkdirAll(path.Dir(filePath), 0700); err != nil {
return
}
}
@ -121,6 +121,8 @@ func CreateDatabase(driver string, db *gorm.DB) error {
&models.ItemIcon{},
&models.UserConfig{},
&models.File{},
&models.ItemIconGroup{},
&models.ModuleConfig{},
)
return err

View File

@ -5,7 +5,6 @@ import (
"crypto/md5"
"encoding/hex"
"fmt"
"io/ioutil"
"math/rand"
"os"
"path"
@ -198,7 +197,7 @@ func AssetsTakeFileToPath(assetsPath, targetPath string) error {
return err
}
}
return ioutil.WriteFile(targetPath, bytes, 0666)
return os.WriteFile(targetPath, bytes, 0666)
}
// 密码加密

View File

@ -93,7 +93,7 @@ func NewLog(log_file_name string) *LogStruct {
logDir := path.Dir(log_file_name)
ok, _ := PathExists(logDir)
if !ok {
if err := os.MkdirAll(logDir, 0666); err != nil {
if err := os.MkdirAll(logDir, 0700); err != nil {
fmt.Println("创建日志文件错误", err.Error())
}
}
@ -128,7 +128,7 @@ func RunLog() *LogStruct {
runLogStatic.Writer = io.MultiWriter(f)
} else {
if runLogStatic.File == nil {
f, _ := os.OpenFile(log_file_name, os.O_APPEND|os.O_WRONLY, 0666)
f, _ := os.OpenFile(log_file_name, os.O_APPEND|os.O_WRONLY, 0700)
runLogStatic.File = f
runLogStatic.Writer = io.MultiWriter(f)
}

View File

@ -4,5 +4,6 @@ type ItemIconIconInfo struct {
ItemType int `json:"itemType"`
Src string `json:"src"`
Text string `json:"text"`
BgColor string `json:"bgColor"`
// BgColor string `json:"bgColor"`
BackgroundColor string `json:"backgroundColor"`
}

View File

@ -1,18 +1,31 @@
package models
import "sun-panel/models/datatype"
import (
"sun-panel/models/datatype"
"gorm.io/gorm"
)
type ItemIcon struct {
BaseModel
IconJson string `gorm:"type:varchar(1000)" json:"-"`
Icon datatype.ItemIconIconInfo `gorm:"-" json:"icon"`
Title string `gorm:"type:varchar(50)" json:"title"`
Url string `gorm:"type:varchar(1000)" json:"url"`
LanUrl string `gorm:"type:varchar(1000)" json:"lanUrl"`
Description string `gorm:"type:varchar(1000)" json:"description"`
OpenMethod int `gorm:"type:tinyint(1)" json:"openMethod"`
Sort int `gorm:"type:int(11)" json:"sort"`
GroupId int `json:"groupId"` // 为以后分组做准备
UserId uint `json:"userId"`
User User `json:"user"`
IconJson string `gorm:"type:varchar(1000)" json:"-"`
Icon datatype.ItemIconIconInfo `gorm:"-" json:"icon"`
Title string `gorm:"type:varchar(50)" json:"title"`
Url string `gorm:"type:varchar(1000)" json:"url"`
LanUrl string `gorm:"type:varchar(1000)" json:"lanUrl"`
Description string `gorm:"type:varchar(1000)" json:"description"`
OpenMethod int `gorm:"type:tinyint(1)" json:"openMethod"`
Sort int `gorm:"type:int(11)" json:"sort"`
ItemIconGroupId int `json:"itemIconGroupId"`
UserId uint `json:"userId"`
User User `json:"user"`
}
func (m *ItemIcon) DeleteByItemIconGroupIds(db *gorm.DB, userId uint, itemIconGroupIds []uint) (err error) {
err = db.Delete(&ItemIcon{}, "item_icon_group_id in ? AND user_id=?", itemIconGroupIds, userId).Error
return
}
func (m *ItemIcon) DeleteByUserId(db *gorm.DB, userId uint) (err error) {
return db.Delete(&ItemIcon{}, "user_id=?", userId).Error
}

View File

@ -0,0 +1,20 @@
package models
import (
"gorm.io/gorm"
)
type ItemIconGroup struct {
BaseModel
Icon string `json:"icon"`
Title string `gorm:"type:varchar(50)" json:"title"`
Description string `gorm:"type:varchar(1000)" json:"description"`
Sort int `gorm:"type:int(11)" json:"sort"`
UserId uint `json:"userId"`
User User `json:"user"`
}
func (m *ItemIconGroup) DeleteByUserId(db *gorm.DB, userId uint) (err error) {
err = db.Delete(&ItemIconGroup{}, "user_id = ?", userId).Error
return
}

View File

@ -0,0 +1,61 @@
package models
import (
"encoding/json"
"gorm.io/gorm"
)
type ModuleConfig struct {
BaseModel
UserId uint `gorm:"index" json:"userId"`
Name string `gorm:"type:varchar(255)" json:"name"`
ValueJson string `gorm:"type:text" json:"-"`
Value map[string]interface{} `gorm:"-" json:"value"`
}
func (m *ModuleConfig) GetConfigByUserIdAndName(db *gorm.DB, userId uint, name string) (map[string]interface{}, error) {
cfg := ModuleConfig{}
if err := db.First(&cfg, "user_id=? AND name=?", userId, name).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
} else {
return nil, err
}
}
// 处理字段
if err := json.Unmarshal([]byte(cfg.ValueJson), &cfg.Value); err != nil {
cfg.Value = nil
}
return cfg.Value, nil
}
func (m *ModuleConfig) Save(db *gorm.DB) error {
// 处理字段
if jb, err := json.Marshal(m.Value); err != nil {
m.ValueJson = "{}"
} else {
m.ValueJson = string(jb)
}
// 保存操作
if err := db.First(&ModuleConfig{}, "user_id=? AND name=?", m.UserId, m.Name).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// 新增
if err := db.Create(&m).Error; err != nil {
return err
}
} else {
return err
}
} else {
// 修改
if err := db.Select("Name", "UserId", "ValueJson").Where("user_id=? AND name=?", m.UserId, m.Name).Updates(&m).Error; err != nil {
return err
}
}
return nil
}

View File

@ -6,4 +6,5 @@ func Init(routerGroup *gin.RouterGroup) {
InitItemIcon(routerGroup)
InitUserConfig(routerGroup)
InitUsersRouter(routerGroup)
InitItemIconGroup(routerGroup)
}

View File

@ -14,5 +14,6 @@ func InitItemIcon(router *gin.RouterGroup) {
r.POST("/panel/itemIcon/edit", itemIcon.Edit)
r.POST("/panel/itemIcon/getListByGroupId", itemIcon.GetListByGroupId)
r.POST("/panel/itemIcon/deletes", itemIcon.Deletes)
r.POST("/panel/itemIcon/saveSort", itemIcon.SaveSort)
}
}

View File

@ -0,0 +1,19 @@
package panel
import (
"sun-panel/api/api_v1"
"sun-panel/api/api_v1/middleware"
"github.com/gin-gonic/gin"
)
func InitItemIconGroup(router *gin.RouterGroup) {
itemIconGroup := api_v1.ApiGroupApp.ApiPanel.ItemIconGroup
r := router.Group("", middleware.LoginInterceptor)
{
r.POST("/panel/itemIconGroup/edit", itemIconGroup.Edit)
r.POST("/panel/itemIconGroup/getList", itemIconGroup.GetList)
r.POST("/panel/itemIconGroup/deletes", itemIconGroup.Deletes)
r.POST("/panel/itemIconGroup/saveSort", itemIconGroup.SaveSort)
}
}

View File

@ -10,4 +10,5 @@ func Init(routerGroup *gin.RouterGroup) {
InitCaptchaRouter(routerGroup)
InitRegister(routerGroup)
InitNoticeRouter(routerGroup)
InitModuleConfigRouter(routerGroup)
}

View File

@ -0,0 +1,16 @@
package system
import (
"sun-panel/api/api_v1"
"sun-panel/api/api_v1/middleware"
"github.com/gin-gonic/gin"
)
func InitModuleConfigRouter(router *gin.RouterGroup) {
api := api_v1.ApiGroupApp.ApiSystem.ModuleConfigApi
r := router.Group("", middleware.LoginInterceptor)
r.POST("/system/moduleConfig/getByName", api.GetByName)
r.POST("/system/moduleConfig/save", api.Save)
}

View File

@ -14,9 +14,10 @@ export function edit<T>(req: Panel.ItemInfo) {
// })
// }
export function getListByGroupId<T>() {
export function getListByGroupId<T>(itemIconGroupId: number | undefined) {
return post<T>({
url: '/panel/itemIcon/getListByGroupId',
data: { itemIconGroupId },
})
}
@ -26,3 +27,10 @@ export function deletes<T>(ids: number[]) {
data: { ids },
})
}
export function saveSort<T>(data: Panel.ItemIconSortRequest) {
return post<T>({
url: '/panel/itemIcon/saveSort',
data,
})
}

View File

@ -0,0 +1,28 @@
import { post } from '@/utils/request'
export function edit<T>(req: Panel.ItemIconGroup) {
return post<T>({
url: '/panel/itemIconGroup/edit',
data: req,
})
}
export function getList<T>() {
return post<T>({
url: '/panel/itemIconGroup/getList',
})
}
export function deletes<T>(ids: number[]) {
return post<T>({
url: '/panel/itemIconGroup/deletes',
data: { ids },
})
}
export function saveSort<T>(sortItems: Common.SortItemRequest[]) {
return post<T>({
url: '/panel/itemIconGroup/saveSort',
data: { sortItems },
})
}

View File

@ -0,0 +1,18 @@
import { post } from '@/utils/request'
export function getValueByName<T>(name: string) {
return post<T>({
url: '/system/moduleConfig/getByName',
data: { name },
})
}
export function save<T>(name: string, value: any) {
return post<T>({
url: '/system/moduleConfig/save',
data: {
name,
value,
},
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 566.93 566.93"><defs><style>.cls-1{fill:#4fb3bb;}</style></defs><path class="cls-1" d="M99.37,502.4H345.56A156.16,156.16,0,0,0,501.72,346.25h0A156.16,156.16,0,0,0,345.56,190.1h-123A33.54,33.54,0,0,0,189,223.61v1a33.54,33.54,0,0,0,33.54,33.56H344.44c48.51,0,88.77,38.74,89.24,87.25a88.22,88.22,0,0,1-88.12,89s-163.09.37-245.94-.27a34.15,34.15,0,0,0-34.41,34.15h0A34.15,34.15,0,0,0,99.37,502.4Z"/><path class="cls-1" d="M467.56,64.53H221.37A156.15,156.15,0,0,0,65.21,220.68h0A156.15,156.15,0,0,0,221.37,376.83h123a33.54,33.54,0,0,0,33.54-33.51v-1a33.54,33.54,0,0,0-33.54-33.56H222.49c-48.52,0-88.77-38.74-89.24-87.25a88.22,88.22,0,0,1,88.12-89s163.09-.37,245.94.27a34.15,34.15,0,0,0,34.41-34.15h0A34.15,34.15,0,0,0,467.56,64.53Z"/></svg>

After

Width:  |  Height:  |  Size: 823 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -0,0 +1,20 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.2739 83.9026C24.8696 84.9049 27.8614 87.6111 34.3437 84.6042C37.8342 82.6999 40.128 81.2967 41.3247 80.3947L40.826 80.6953L40.9258 54.3354L40.7263 17.9527C40.7263 17.9527 40.0282 12.7409 36.1388 10.2352C31.8505 6.82742 24.8696 2.01648 23.972 1.7158C23.1742 1.0142 20.1824 -0.0883057 18.2875 3.62013L18.3873 76.6862C18.3873 76.6862 18.487 77.4881 18.6864 78.791C19.8832 80.6953 21.4788 82.9004 23.2739 83.9026Z" fill="url(#paint0_linear_102_2)"/>
<path d="M70.2458 62.7546L41.3247 80.2944C40.0282 81.1965 37.8342 82.5997 34.244 84.504C27.8614 87.5109 24.8696 84.8047 23.1742 83.8024C21.3791 82.8001 19.7835 80.5951 18.5867 78.5906C19.1851 81.397 20.5813 86.4083 24.1715 89.9163C28.759 94.627 34.3437 100.741 49.4027 98.5359C52.7934 97.5336 56.4833 95.5291 63.0654 91.1191C68.0518 87.8115 73.1379 86.1077 77.3265 80.2944C82.0137 67.3651 72.739 63.5564 70.2458 62.7546Z" fill="url(#paint1_linear_102_2)"/>
<path d="M80.8169 54.0347C77.6256 47.9208 77.127 45.3149 67.1542 39.5017C56.9819 34.2898 55.486 33.488 55.486 33.488C55.486 33.488 52.9928 31.3832 50.5993 31.5837C48.3056 32.586 46.8097 33.9891 48.5051 38.3992C50.7988 44.7135 53.4915 51.6293 53.4915 51.6293C53.4915 51.6293 54.4887 56.7409 58.5776 58.4447C65.6583 60.75 70.2457 62.6543 70.2457 62.6543L70.146 62.7545C72.6392 63.5564 81.9139 67.2648 77.3264 80.3946C78.8223 78.3901 80.1188 75.7842 81.3155 72.5769C82.8115 66.9641 83.5096 64.1577 80.8169 54.0347Z" fill="url(#paint2_linear_102_2)"/>
<defs>
<linearGradient id="paint0_linear_102_2" x1="10.1209" y1="6.26564" x2="45.1332" y2="79.3835" gradientUnits="userSpaceOnUse">
<stop stop-color="#3BBDF5"/>
<stop offset="1" stop-color="#1B45D9"/>
</linearGradient>
<linearGradient id="paint1_linear_102_2" x1="18.7266" y1="80.9445" x2="78.701" y2="80.9445" gradientUnits="userSpaceOnUse">
<stop stop-color="#5AD9FE"/>
<stop offset="0.51" stop-color="#43AAE1"/>
<stop offset="1" stop-color="#144AD5"/>
</linearGradient>
<linearGradient id="paint2_linear_102_2" x1="55.3044" y1="46.4248" x2="82.9308" y2="67.615" gradientUnits="userSpaceOnUse">
<stop stop-color="#3DC0FE"/>
<stop offset="1" stop-color="#61DDD5"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639468659890" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3509" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><defs><style type="text/css"></style></defs><path d="M214.101333 512c0-32.512 5.546667-63.701333 15.36-92.928L57.173333 290.218667A491.861333 491.861333 0 0 0 4.693333 512c0 79.701333 18.858667 154.88 52.394667 221.610667l172.202667-129.066667A290.56 290.56 0 0 1 214.101333 512" fill="#FBBC05" p-id="3510"></path><path d="M516.693333 216.192c72.106667 0 137.258667 25.002667 188.458667 65.96266699L854.101333 136.533333C763.349333 59.178667 646.997333 11.392 516.693333 11.392c-202.325333 0-376.234667 113.28-459.52 278.826667l172.373334 128.853333c39.68-118.016 152.832-202.88 287.146666-202.88" fill="#EA4335" p-id="3511"></path><path d="M516.693333 807.808c-134.357333 0-247.509333-84.864-287.232-202.88l-172.288 128.853333c83.242667 165.546667 257.152 278.826667 459.52 278.826667 124.842667 0 244.053333-43.392 333.568-124.757333l-163.584-123.818667c-46.122667 28.458667-104.234667 43.776-170.026666 43.776" fill="#34A853" p-id="3512"></path><path d="M1005.397333 512c0-29.568-4.693333-61.44-11.64799999-91.008H516.650667V614.4h274.602666c-13.696 65.962667-51.072 116.650667-104.533333 149.632l163.541333 123.818667c93.994667-85.418667 155.136-212.650667 155.136-375.850667" fill="#4285F4" p-id="3513"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,40 +1,44 @@
<script setup lang="ts">
import { NAvatar, NImage } from 'naive-ui'
import { computed, withDefaults } from 'vue'
import { computed, ref, withDefaults } from 'vue'
import { SvgIcon } from '@/components/common'
interface Prop {
itemIcon?: Panel.ItemIcon | null
size?: number // 70
forceBackground?: string //
}
const props = withDefaults(defineProps<Prop>(), { size: 70 })
const defaultStyle = { width: `${props.size}px`, height: `${props.size}px` }
const defaultBackground = '#2a2a2a6b'
const defaultStyle = ref({
width: `${props.size}px`,
height: `${props.size}px`,
})
const iconExt = computed(() => {
return props.itemIcon?.src?.split('.').pop()
})
</script>
<template>
<div class="overflow-hidden rounded-2xl" :style="defaultStyle">
<div :style="defaultStyle">
<slot>
<div v-if="itemIcon">
<div v-if="itemIcon?.itemType === 1">
<NAvatar :size="props.size" :style="{ backgroundColor: itemIcon?.bgColor }">
<NAvatar :size="props.size" :style="{ backgroundColor: (forceBackground ?? itemIcon?.backgroundColor) || defaultBackground }">
{{ itemIcon.text }}
</NAvatar>
</div>
<div v-else-if="itemIcon?.itemType === 2">
<div v-if="iconExt === 'svg'" :style="defaultStyle" class="flex justify-center items-center">
<div v-if="iconExt === 'svg'" :style="{ backgroundColor: (forceBackground ?? itemIcon?.backgroundColor) || defaultBackground, ...defaultStyle }" class="flex justify-center items-center">
<img :src="itemIcon?.src" class="w-[35px] h-[35px]">
<!-- <object :data="itemIcon?.src" type="image/svg+xml" class="w-[35px] h-[35px]" style="fill: rgb(255, 255, 255) !important;" /> -->
</div>
<NImage v-else :style="defaultStyle" :src="itemIcon?.src" preview-disabled />
<NImage v-else :style="{ backgroundColor: (forceBackground ?? itemIcon?.backgroundColor) || defaultBackground, ...defaultStyle }" :src="itemIcon?.src" preview-disabled />
</div>
<div v-else-if="itemIcon?.itemType === 3">
<NAvatar :size="props.size" :style="{ backgroundColor: itemIcon?.bgColor }">
<NAvatar :size="props.size" :style="{ backgroundColor: (forceBackground ?? itemIcon?.backgroundColor) || defaultBackground }">
<SvgIcon style="font-size: 35px;" :icon="itemIcon.text" />
</NAvatar>
</div>

View File

@ -30,7 +30,7 @@ const showModal = computed({
</script>
<template>
<NModal v-model:show="showModal" preset="card" :size="size" v-bind="bindAttrs" style="border-radius: 1rem;width: 600px;" :title="title">
<NModal v-model:show="showModal" preset="card" :size="size" v-bind="bindAttrs" style="border-radius: 1rem;" :style="$parent" :title="title">
<template #cover>
<slot name="cover" />
</template>

View File

@ -50,33 +50,6 @@ async function getAboutDescription() {
<div>
<span v-html="content" />
</div>
<!-- <div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
<p>
此项目开源于
<a
class="text-blue-600 dark:text-blue-500"
href="https://github.com/Chanzhaoyu/chatgpt-web"
target="_blank"
>
GitHub
</a>
免费且基于 MIT 协议没有任何形式的付费行为
</p>
<p>
如果你觉得此项目对你有帮助请在 GitHub 帮我点个 Star 或者给予一点赞助谢谢
</p>
</div> -->
<!-- <p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p v-if="isChatGPTAPI">
{{ $t("setting.monthlyUsage") }}{{ config?.usage ?? '-' }}
</p>
<p v-if="!isChatGPTAPI">
{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}
</p>
<p>{{ $t("setting.timeout") }}{{ config?.timeoutMs ?? '-' }}</p>
<p>{{ $t("setting.socks") }}{{ config?.socksProxy ?? '-' }}</p>
<p>{{ $t("setting.httpsProxy") }}{{ config?.httpsProxy ?? '-' }}</p> -->
</div>
</NSpin>
</template>

View File

@ -1,23 +1,178 @@
<script setup lang="ts">
import { NInput } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { NAvatar, NCheckbox } from 'naive-ui'
import { SvgIcon } from '@/components/common'
import { useModuleConfig } from '@/store/modules'
import SvgSrcBaidu from '@/assets/search_engine_svg/baidu.svg'
import SvgSrcBing from '@/assets/search_engine_svg/bing.svg'
import SvgSrcGoogle from '@/assets/search_engine_svg/google.svg'
interface State {
currentSearchEngine: DeskModule.SearchBox.SearchEngine
searchEngineList: DeskModule.SearchBox.SearchEngine[]
newWindowOpen: boolean
}
withDefaults(defineProps<{
background?: string
textColor?: string
}>(), {
background: '#2a2a2a6b',
textColor: 'white',
})
const moduleConfigName = 'deskModuleSearchBox'
const moduleConfig = useModuleConfig()
const searchTerm = ref('')
const isFocused = ref(false)
const searchSelectListShow = ref(false)
const defaultSearchEngineList = ref<DeskModule.SearchBox.SearchEngine[]>([
{
iconSrc: SvgSrcGoogle,
title: 'Google',
url: 'https://www.google.com/search?q=%s',
},
{
iconSrc: SvgSrcBaidu,
title: 'Baidu',
url: 'https://www.baidu.com/s?wd=%s',
},
{
iconSrc: SvgSrcBing,
title: 'Bing',
url: 'https://www.bing.com/search?q=%s',
},
])
const defaultState: State = {
currentSearchEngine: defaultSearchEngineList.value[0],
searchEngineList: [] || defaultSearchEngineList,
newWindowOpen: false,
}
const state = ref<State>({ ...defaultState })
const onFocus = (): void => {
isFocused.value = true
}
const onBlur = (): void => {
isFocused.value = false
}
function handleEngineClick() {
searchSelectListShow.value = !searchSelectListShow.value
}
function handleEngineUpdate(engine: DeskModule.SearchBox.SearchEngine) {
state.value.currentSearchEngine = engine
moduleConfig.saveToCloud(moduleConfigName, state.value)
}
function handleSearchClick() {
const url = state.value.currentSearchEngine.url
const keyword = searchTerm
// %s
const fullUrl = replaceOrAppendKeywordToUrl(url, keyword.value)
if (state.value.newWindowOpen)
window.open(fullUrl)
else
window.location.href = fullUrl
}
function replaceOrAppendKeywordToUrl(url: string, keyword: string) {
// %s
if (url.includes('%s'))
return url.replace('%s', encodeURIComponent(keyword))
// %s
return url + (keyword ? `${encodeURIComponent(keyword)}` : '')
}
onMounted(() => {
moduleConfig.getValueByNameFromCloud<State>('deskModuleSearchBox').then(({ code, data }) => {
if (code === 0)
state.value = data || defaultState
else
state.value = defaultState
})
})
</script>
<template>
<div class="w-full">
<NInput size="large" class="background" round placeholder="输入搜索内容">
<template #prefix>
百度
</template>
<template #suffix>
<div class="w-full" @keydown.enter="handleSearchClick">
<div class="search-container flex rounded-2xl items-center justify-center text-white w-full" :style="{ background, color: textColor }" :class="{ focused: isFocused }">
<div class="w-[40px] flex justify-center cursor-pointer" @click="handleEngineClick">
<NAvatar :src="state.currentSearchEngine.iconSrc" style="background-color: transparent;" :size="20" />
</div>
<input v-model="searchTerm" placeholder="请输入搜索内容" @focus="onFocus" @blur="onBlur">
<div class="w-[20px] flex justify-center cursor-pointer" @click="handleSearchClick">
<SvgIcon icon="iconamoon:search-fill" />
</template>
</NInput>
</div>
</div>
<!-- 搜索引擎选择 -->
<div v-if="searchSelectListShow" class="w-full mt-[10px] rounded-xl p-[10px]" :style="{ background }">
<div class="flex items-center">
<div class="flex items-center">
<div
v-for="item, index in defaultSearchEngineList"
:key="index"
:title="item.title"
class="w-[40px] h-[40px] mr-[10px] cursor-pointer bg-[#ffffff] flex items-center justify-center rounded-xl"
@click="handleEngineUpdate(item)"
>
<NAvatar :src="item.iconSrc" style="background-color: transparent;" :size="20" />
</div>
<!-- <div class="w-[40px] h-[40px] ml-[10px] flex justify-center items-center cursor-pointer" @click="handleEngineClick">
<NAvatar style="background-color: transparent;" :size="30">
<SvgIcon icon="lets-icons:setting-alt-fill" style="font-size: 20px;" />
</NAvatar>
</div> -->
</div>
</div>
<div class="mt-[10px]">
<NCheckbox v-model:checked="state.newWindowOpen" @update-checked="moduleConfig.saveToCloud(moduleConfigName, state)">
<span :style="{ color: textColor }">
新窗口打开
</span>
</NCheckbox>
</div>
</div>
</div>
</template>
<style scoped>
.background{
background-color: #ffffff78;
.search-container {
border: 1px solid #ccc;
transition: border-color 0.5s;
padding: 2px 10px;
}
.focused {
border-color: white;
}
.before {
left: 10px;
}
.after {
right: 10px;
}
input {
background-color: transparent;
box-sizing: border-box;
width: 100%;
height: 40px;
padding: 10px 5px;
border: none;
outline: none;
font-size: 17px;
}
</style>

1
src/enums/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './panel'

View File

@ -6,6 +6,6 @@ export enum PanelStateNetworkModeEnum {
}
export enum PanelPanelConfigStyleEnum {
'icon' = 0, // 图标风格
'info' = 1, // 详情风格
'icon' = 1, // 图标风格
'info' = 0, // 详情风格
}

View File

@ -7,16 +7,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
// component: () => import('@/views/home/Layout.vue'),
component: () => import('@/views/home/index.vue'),
// children: [
// {
// path: '/edit/:noteId?',
// name: 'EditNote',
// component: () => import('@/views/home/index.vue'),
// },
// ],
},
{

View File

@ -5,3 +5,4 @@ export * from './auth'
export * from './admin'
export * from './notice'
export * from './panel'
export * from './moduleConfig'

View File

@ -0,0 +1,22 @@
import { ss } from '@/utils/storage'
// import userDefaultAvatar from '@/assets/userDefaultAvatar.png'
const LOCAL_NAME = 'moduleConfig'
export interface Config {
name: string
config: any
}
export interface ModuleConfigState {
[key: string]: any
}
export function getLocalState(): ModuleConfigState {
const localSetting: ModuleConfigState | undefined = ss.get(LOCAL_NAME)
return { ...localSetting }
}
export function setLocalState(setting: ModuleConfigState): void {
ss.set(LOCAL_NAME, setting)
}

View File

@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
import type { ModuleConfigState } from './helper'
import { getLocalState, setLocalState } from './helper'
import { getValueByName, save } from '@/api/system/moduleConfig'
export const useModuleConfig = defineStore('module-config-store', {
state: (): ModuleConfigState => getLocalState(),
actions: {
// 保存
// save(name: string, value: any) {
// const moduleName = `module-${name}`
// // 保存至网络
// console.log('保存模块配置', name, value)
// this.$state[moduleName] = value
// this.recordState()
// save(moduleName, value)
// },
// // 获取值
// getValueByName<T>(name: string): T | null {
// const moduleName = `module-${name}`
// this.syncFromCloud(moduleName)
// if (this.$state[moduleName])
// return this.$state[moduleName]
// return null
// },
// 获取值
async getValueByNameFromCloud<T>(name: string) {
const moduleName = `module-${name}`
return await getValueByName<T>(moduleName)
},
// 保存到网络
saveToCloud(name: string, value: any) {
const moduleName = `module-${name}`
// 保存至网络
save(moduleName, value)
},
// 从网络同步
// syncFromCloud(moduleName: string) {
// getValueByName<any>(moduleName).then(({ code, data, msg }) => {
// if (code === 0)
// this.$state[moduleName] = data
// })
// },
recordState() {
setLocalState(this.$state)
},
},
})

View File

@ -1,5 +1,5 @@
import { ss } from '@/utils/storage'
import { PanelPanelConfigStyleEnum, PanelStateNetworkModeEnum } from '@/enum'
import { PanelPanelConfigStyleEnum, PanelStateNetworkModeEnum } from '@/enums'
import defaultBackground from '@/assets/defaultBackground.webp'
const LOCAL_NAME = 'panelStorage'
@ -7,11 +7,15 @@ export function defaultStatePanelConfig(): Panel.panelConfig {
return {
backgroundImageSrc: defaultBackground,
backgroundBlur: 0,
backgroundMaskNumber: 0,
iconStyle: PanelPanelConfigStyleEnum.icon,
iconTextColor: '#ffffff',
iconTextInfoHideDescription: false,
iconTextIconHideTitle: false,
logoText: 'Sun-Panel',
logoImageSrc: '',
clockShowSecond: false,
searchBoxShow: false,
}
}

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { defaultState, defaultStatePanelConfig, getLocalState, removeLocalState, setLocalState } from './helper'
import { router } from '@/router'
import type { PanelStateNetworkModeEnum } from '@/enum'
import type { PanelStateNetworkModeEnum } from '@/enums'
import { get as getUserConfig } from '@/api/panel/userConfig'
export const usePanelState = defineStore('panel', {
state: (): Panel.State => getLocalState() || defaultState(),

View File

@ -28,4 +28,9 @@ declare namespace Common {
result?:boolean
message?:string
}
interface SortItemRequest{
id:number
sort:number
}
}

10
src/typings/deskModule/searchBox.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare namespace DeskModule.SearchBox {
interface SearchEngine {
iconSrc: string
title: string
url: string
}
}

View File

@ -11,13 +11,21 @@ declare namespace Panel {
lanUrl?: string
description?: string
openMethod: number
itemIconGroupId ?:number
}
interface ItemIconGroup extends Common.InfoBase {
icon?: string
title?: string
sort?:number
}
interface ItemIcon {
itemType: number
src ?: string
text ?: string
bgColor ?: string
// bgColor ?: string
backgroundColor ?: string
}
interface State {
@ -30,18 +38,26 @@ declare namespace Panel {
interface panelConfig{
backgroundImageSrc?:string
backgroundBlur?:number
backgroundMaskNumber?:number
iconStyle?:PanelPanelConfigStyleEnum
iconTextColor?:string
iconTextInfoHideDescription?:boolean
iconTextIconHideTitle?:boolean
logoText?:string
logoImageSrc?:string
clockShowSecond?:boolean
clockColor?:string
searchBoxShow?:boolean
}
interface userConfig{
panel:panelConfig
searchEngine?:any
}
interface ItemIconSortRequest{
sortItems:Common.SortItemRequest[]
itemIconGroupId:number
}
}

View File

@ -1,33 +1,41 @@
<script setup lang="ts">
import RecursiveList from './RecursiveList.vue'
import { computed, ref } from 'vue'
import { NColorPicker } from 'naive-ui'
const multiLevelData = [
{
name: '开发笔记',
children: [
{
name: 'Level 2 Item 1',
extand: true,
children: [
{ name: 'SAI-Chat开发' },
{ name: '笔记本项目' },
],
},
{ name: '测试项目' },
],
},
{
name: '学习笔记',
children: [
{ name: 'Blender' },
{ name: 'mongo' },
],
},
]
// RGB
const bgColor = ref('#000000') //
//
const calculateLuminance = (color: string) => {
const hex = color.replace(/^#/, '')
const r = parseInt(hex.substring(0, 2), 16)
const g = parseInt(hex.substring(2, 4), 16)
const b = parseInt(hex.substring(4, 6), 16)
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
}
//
const textColor = computed(() => {
const luminance = calculateLuminance(bgColor.value)
return luminance > 0.5 ? 'black' : 'white'
})
</script>
<template>
<div>
<RecursiveList :items="multiLevelData" />
<div :style="{ backgroundColor: bgColor, color: textColor }">
Background Color Example
</div>
<NColorPicker v-model:value="bgColor" />
</template>
<style scoped>
/* 样式可以根据实际需求自定义 */
div {
width: 200px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
}
</style>

View File

@ -1,50 +0,0 @@
<script setup lang='ts'>
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui'
import { computed } from 'vue'
// import { Header } from './components'
// import { LeftSider, RightSider } from './layout'
import Index from './index.vue'
import { usePanelState } from '@/store/modules'
const panelState = usePanelState()
const leftSiderCollapsed = computed(() => panelState.leftSiderCollapsed)
const rightSiderCollapsed = computed(() => panelState.rightSiderCollapsed)
</script>
<template>
<div class="h-full">
<NLayout has-sider class="h-full">
<NLayoutSider
v-model:collapsed="leftSiderCollapsed"
collapse-mode="transform"
:collapsed-width="0"
:width="240"
bordered
>
<LeftSider />
</NLayoutSider>
<NLayoutContent>
<NLayout has-sider sider-placement="right" class="h-full">
<NLayoutContent class="h-full">
<!-- 内容 -->
<!-- <Header /> -->
<div>
<Index />
</div>
</NLayoutContent>
<NLayoutSider
v-model:collapsed="rightSiderCollapsed"
collapse-mode="transform"
:collapsed-width="0"
:width="280"
content-style="padding: 20px;"
bordered
>
<RightSider />
</NLayoutSider>
</NLayout>
</NLayoutContent>
</NLayout>
</div>
</template>

View File

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import { computed } from 'vue'
import { NEllipsis } from 'naive-ui'
import { ItemIcon } from '@/components/common'
import { PanelPanelConfigStyleEnum } from '@/enums'
interface Prop {
itemInfo?: Panel.ItemInfo
size?: number // 70
forceBackground?: string //
iconTextColor?: string
iconTextInfoHideDescription: boolean
iconTextIconHideTitle: boolean
style: PanelPanelConfigStyleEnum
}
const props = withDefaults(defineProps<Prop>(), {
size: 70,
})
const defaultBackground = '#2a2a2a6b'
const calculateLuminance = (color: string) => {
const hex = color.replace(/^#/, '')
const r = parseInt(hex.substring(0, 2), 16)
const g = parseInt(hex.substring(2, 4), 16)
const b = parseInt(hex.substring(4, 6), 16)
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
}
const textColor = computed(() => {
const luminance = calculateLuminance(props.itemInfo?.icon?.backgroundColor || defaultBackground)
return luminance > 0.5 ? 'black' : 'white'
})
</script>
<template>
<div class="w-full">
<!-- 详情图标 -->
<div
v-if="style === PanelPanelConfigStyleEnum.info"
class="w-full rounded-2xl transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] flex"
:style="{ background: itemInfo?.icon?.backgroundColor || defaultBackground }"
>
<!-- 图标 -->
<div class="w-[70px] h-[70px]">
<div class="w-[70px] h-full flex items-center justify-center ">
<ItemIcon :item-icon="itemInfo?.icon" force-background="transparent" :size="50" class="overflow-hidden rounded-xl" />
</div>
</div>
<!-- 文字 -->
<!-- 如果为纯白色将自动根据背景的明暗计算字体的黑白色 -->
<div class="text-white flex items-center" :style="{ color: (iconTextColor === '#ffffff') ? textColor : iconTextColor, maxWidth: 'calc(100% - 80px)' }">
<div class="w-full">
<div class="font-semibold w-full">
<NEllipsis>
{{ itemInfo?.title }}
</NEllipsis>
</div>
<div v-if="!iconTextInfoHideDescription">
<NEllipsis :line-clamp="2" class="text-xs">
{{ itemInfo?.description }}
</NEllipsis>
</div>
</div>
</div>
</div>
<!-- 极简图标APP -->
<div v-if="style === PanelPanelConfigStyleEnum.icon">
<div
class="overflow-hidden rounded-2xl sunpanel w-[70px] h-[70px] mx-auto rounded-2xl transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)]"
:title="itemInfo?.description"
>
<ItemIcon :item-icon="itemInfo?.icon" />
</div>
<div
v-if="!iconTextIconHideTitle"
class="text-center app-icon-text-shadow cursor-pointer mt-[2px]"
:style="{ color: iconTextColor }"
>
<span>{{ itemInfo?.title }}</span>
</div>
</div>
</div>
</template>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { NButton, NColorPicker, NInput, NRadio, NUpload, useMessage } from 'naive-ui'
import type { UploadFileInfo } from 'naive-ui'
import { defineProps, ref } from 'vue'
import { defineProps, ref, watch } from 'vue'
import { ItemIcon } from '@/components/common'
import { useAuthStore } from '@/store'
@ -17,19 +17,27 @@ const checkedValueRef = ref<number | null>(props.itemIcon?.itemType || 1)
//
const defautSwatchesBackground = [
'#000',
'#00000000',
'#000000',
'#ffffff',
'#18A058',
'#2080F0',
'#F0A020',
'rgba(208, 48, 80, 1)',
'#C418D1FF',
]
const initData: Panel.ItemIcon = {
itemType: 1,
bgColor: '#000',
backgroundColor: '#2a2a2a6b',
}
const itemIconInfo = ref<Panel.ItemIcon>(props.itemIcon ? { ...props.itemIcon } : { ...initData })
// const itemIconInfo = ref<Panel.ItemIcon>(props.itemIcon ?? { ...initData })
const itemIconInfo = ref<Panel.ItemIcon>({
...initData,
...props.itemIcon,
backgroundColor: props.itemIcon?.backgroundColor || initData.backgroundColor,
})
function handleIconTypeRadioChange(type: number) {
checkedValueRef.value = type
@ -38,7 +46,6 @@ function handleIconTypeRadioChange(type: number) {
}
function handleChange() {
// console.log('', itemIconInfo.value)
emit('update:itemIcon', itemIconInfo.value || null)
}
@ -61,6 +68,10 @@ const handleUploadFinish = ({
return file
}
watch(itemIconInfo.value, () => {
handleChange()
})
</script>
<template>
@ -94,19 +105,57 @@ const handleUploadFinish = ({
</NRadio>
</div>
<div class="flex h-[100px]">
<div>
<div class="border rounded-2xl bg-slate-200">
<ItemIcon :item-icon="itemIconInfo" />
<div class=" h-[100px]">
<div class="flex">
<div>
<div class="border rounded-2xl bg-slate-200 overflow-hidden rounded-2xl transparent-grid">
<ItemIcon :item-icon="itemIconInfo" />
</div>
</div>
<!-- 文字 -->
<div class="ml-[20px]">
<!-- <NImage :src="model.icon" preview-disabled /> -->
<div v-if="checkedValueRef === 1">
<NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入文字作为图标" @input="handleChange" />
</div>
<div v-if="checkedValueRef === 3">
<div>
<NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入图标名字" @input="handleChange" />
<NButton quaternary type="info">
<a target="_blank" href="https://icon-sets.iconify.design/">图标库</a>
</NButton>
</div>
</div>
<!-- 图片 -->
<div v-if="checkedValueRef === 2">
<NInput v-model:value="itemIconInfo.src" class="mb-[5px] w-full" size="small" type="text" placeholder="输入图标地址或上传" @input="handleChange" />
<NUpload
action="/api/file/uploadImg"
:show-file-list="false"
name="imgfile"
:headers="{
token: authStore.token as string,
}"
@finish="handleUploadFinish"
>
<NButton size="small">
点击上传
</NButton>
</NUpload>
</div>
</div>
</div>
<!-- 文字 -->
<div class="ml-[20px]">
<!-- <NImage :src="model.icon" preview-disabled /> -->
<div v-if="checkedValueRef === 1">
<NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入文字作为图标" @input="handleChange" />
<div class="flex items-center mt-[10px]">
<div class="w-auto text-slate-500 mr-[10px]">
背景色:
</div>
<div class="w-[150px] flex items-center mr-[10px]">
<NColorPicker
v-model:value="itemIconInfo.bgColor"
v-model:value="itemIconInfo.backgroundColor"
size="small"
:modes="['hex']"
:swatches="defautSwatchesBackground"
@ -114,39 +163,21 @@ const handleUploadFinish = ({
@update-value="handleChange"
/>
</div>
<div v-if="checkedValueRef === 3">
<div>
<NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入图标名字" @input="handleChange" />
<a target="_blank" href="https://icon-sets.iconify.design/" class="text-[blue]">图标库</a>
</div>
<NColorPicker
v-model:value="itemIconInfo.bgColor"
size="small"
:modes="['hex']"
:swatches="defautSwatchesBackground"
@complete="handleChange"
@update-value="handleChange"
/>
</div>
<!-- 图片 -->
<div v-if="checkedValueRef === 2">
<NUpload
action="/api/file/uploadImg"
:show-file-list="false"
name="imgfile"
:headers="{
token: authStore.token as string,
}"
@finish="handleUploadFinish"
>
<NButton size="small">
点击上传
</NButton>
</NUpload>
<div v-if="itemIconInfo.backgroundColor !== initData.backgroundColor" class="w-auto text-slate-500 mr-[10px] cursor-pointer">
<NButton quaternary type="info" @click="itemIconInfo.backgroundColor = initData.backgroundColor">
恢复默认
</NButton>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.transparent-grid {
background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%),
linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%);
background-size: 16px 16px;
background-position: 0 0, 8px 8px;
}
</style>

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { computed, defineEmits, defineProps, onMounted, ref, watch } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { NButton, NForm, NFormItem, NInput, NModal, NSelect, useMessage } from 'naive-ui'
import { NButton, NForm, NFormItem, NGrid, NGridItem, NInput, NModal, NSelect, useMessage } from 'naive-ui'
import IconEditor from './IconEditor.vue'
import { edit } from '@/api/panel/itemIcon'
import { getList as getGroupList } from '@/api/panel/itemIconGroup'
interface Props {
visible: boolean
@ -13,6 +14,10 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const ms = useMessage()
const itemIconGroupOptions = ref<{
label: string
value: number
}[]>([])
const restoreDefault: Panel.Info = {
icon: null,
@ -29,6 +34,9 @@ interface Emit {
}
const model = ref<Panel.Info>(props.itemInfo !== null ? { ...props.itemInfo } : { ...restoreDefault })
// const model = computed(()=>{
// return props.itemInfo !== null ? { ...props.itemInfo } : { ...restoreDefault }
// })
const formRef = ref<FormInst | null>(null)
const rules: FormRules = {
@ -43,6 +51,11 @@ const rules: FormRules = {
type: 'string',
message: '必填项',
},
// itemIconGroupId: {
// required: true,
// trigger: ['blur', 'change'],
// message: '',
// },
}
const options = [
@ -61,6 +74,22 @@ const options = [
},
]
// const urlProtocolOptions = [
// {
// default: true,
// label: 'http://',
// value: 'http://',
// },
// {
// label: 'https://',
// value: 'https://',
// },
// {
// label: '使',
// value: '',
// },
// ]
//
const show = computed({
get: () => props.visible,
@ -70,15 +99,15 @@ const show = computed({
})
async function editApi() {
const { code, data } = await edit<Panel.ItemInfo>(model.value)
const { code, data, msg } = await edit<Panel.ItemInfo>(model.value)
if (code === 0) {
show.value = false
model.value = restoreDefault
model.value = { ...restoreDefault }
emit('done', data)
}
else {
ms.error('保存失败')
ms.error(`保存失败:${msg}`)
}
}
@ -91,39 +120,77 @@ const handleValidateButtonClick = (e: MouseEvent) => {
}
watch(() => props.itemInfo, (newValue) => {
model.value = newValue || restoreDefault
model.value = newValue || { ...restoreDefault }
getGroupListOptions()
})
function getGroupListOptions() {
getGroupList<Common.ListResponse<Panel.ItemIconGroup[]>>().then(({ data, code, msg }) => {
if (code === 0) {
itemIconGroupOptions.value = []
for (let i = 0; i < data.list.length; i++) {
const element = data.list[i]
if (i === 0 && !model.value.itemIconGroupId) {
restoreDefault.itemIconGroupId = element.id
model.value.itemIconGroupId = element.id
}
itemIconGroupOptions.value.push({
value: element.id as number,
label: element.title as string,
})
}
}
else {
ms.error(`分组信息获取失败:${msg}`)
}
})
}
onMounted(() => {
// rolesLoading.value = true
getGroupListOptions()
})
</script>
<template>
<NModal v-model:show="show" preset="card" style="width: 600px;border-radius: 1rem;" :title="itemInfo ? '修改项目' : '添加项目'">
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem path="title" label="标题">
<NInput v-model:value="model.title" type="text" show-count :maxlength="20" placeholder="请输入标题" />
</NFormItem>
<NFormItem path="icon" label="图标">
<IconEditor v-model:item-icon="model.icon" />
</NFormItem>
<NFormItem path="url" label="跳转地址">
<NInput v-model:value="model.url" type="text" :maxlength="1000" placeholder="请输入跳转地址" />
</NFormItem>
<NFormItem path="lanUrl" label="局域网跳转地址">
<NInput v-model:value="model.lanUrl" type="text" :maxlength="1000" placeholder="(可以留空)切换到局域网模式,点击会使用该地址" />
</NFormItem>
<NFormItem path="description" label="描述信息">
<NInput v-model:value="model.description" type="text" show-count :maxlength="100" placeholder="请填写描述信息" />
</NFormItem>
<NFormItem path="openMethod" label="打开方式">
<NSelect v-model:value="model.openMethod" :options="options" />
</NFormItem>
</NForm>
<div class="h-[600px] overflow-auto p-[5px]">
<NForm ref="formRef" :model="model" :rules="rules">
<NGrid cols="2" :x-gap="10" item-responsive>
<NGridItem span="2 500:1">
<NFormItem path="itemIconGroupId" label="分组">
<NSelect v-model:value="model.itemIconGroupId" :options="itemIconGroupOptions" />
</NFormItem>
</NGridItem>
<NGridItem span="2 500:1">
<NFormItem path="title" label="标题">
<NInput v-model:value="model.title" type="text" show-count :maxlength="20" placeholder="请输入标题" />
</NFormItem>
</NGridItem>
</NGrid>
<NFormItem path="icon" label="图标">
<IconEditor v-model:item-icon="model.icon" />
</NFormItem>
<NFormItem path="url" label="跳转地址">
<!-- <NSelect :style="{ width: '100px' }" :options="urlProtocolOptions" /> -->
<NInput v-model:value="model.url" type="text" :maxlength="1000" placeholder="http(s)://" />
</NFormItem>
<NFormItem path="lanUrl" label="局域网跳转地址">
<NInput v-model:value="model.lanUrl" type="text" :maxlength="1000" placeholder="http(s)://(可以留空,切换到局域网模式,点击会使用该地址)" />
</NFormItem>
<NFormItem path="description" label="描述信息">
<NInput v-model:value="model.description" type="text" show-count :maxlength="100" placeholder="请填写描述信息" />
</NFormItem>
<NFormItem path="openMethod" label="打开方式">
<NSelect v-model:value="model.openMethod" :options="options" />
</NFormItem>
</NForm>
</div>
<template #footer>
<NButton type="success" @click="handleValidateButtonClick">
<NButton type="success" style="float: right;" @click="handleValidateButtonClick">
确定
</NButton>
</template>

View File

@ -5,6 +5,8 @@ import Style from './tabs/Style.vue'
import About from './tabs/About.vue'
import Users from './tabs/Users.vue'
import UserInfo from './tabs/UserInfo.vue'
import ItemGroupManage from './tabs/ItemGroupManage.vue'
import { RoundCardModal } from '@/components/common'
const props = defineProps<{
@ -25,11 +27,14 @@ const show = computed({
<template>
<div>
<RoundCardModal v-model:show="show" title="设置" style="max-height: 700px;">
<NTabs type="line" animated>
<RoundCardModal v-model:show="show" title="设置" style="max-height: 700px;max-width: 600px;">
<NTabs type="line" size="small" animated>
<NTabPane name="style" tab="样式">
<Style />
</NTabPane>
<NTabPane name="itemGroupManage" tab="分组管理">
<ItemGroupManage />
</NTabPane>
<NTabPane name="userInfo" tab="登录信息">
<UserInfo />
</NTabPane>
@ -45,7 +50,7 @@ const show = computed({
</template>
<style scoped>
.text-shadow{
.text-shadow {
text-shadow: 0px 0px 5px gray;
}
</style>

View File

@ -2,6 +2,10 @@
import { NDivider, NGradientText } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { get } from '@/api/system/about'
import srcSvglogo from '@/assets/logo.svg'
import srcGitee from '@/assets/about_image/gitee.png'
import srcGithub from '@/assets/about_image/github.png'
import srcDocker from '@/assets/about_image/docker.png'
interface Version {
versionName: string
@ -20,26 +24,53 @@ onMounted(() => {
<template>
<div>
<div class="text-3xl">
<NGradientText type="danger">
Sun-Panel
</NGradientText>
v{{ versionName }}
<div>
<div class="flex flex-col items-center justify-center">
<img :src="srcSvglogo" width="100" height="100" alt="">
<div class="text-3xl font-semibold">
{{ $t('common.appName') }}
</div>
<div class="text-xl">
<NGradientText type="info">
<a href="https://github.com/hslr-s/sun-panel/releases" class="font-semibold" title="点此查看更新说明" target="_blank">v{{ versionName }}</a>
</NGradientText>
</div>
</div>
</div>
<NDivider />
<div class="text-lg">
开发者 <a href="https://blog.enianteam.com/u/sun/content/11" target="_blank" class="link">红烧猎人</a>
</div>
<div class="text-lg">
项目开源地址
<a href="https://github.com/hslr-s/sun-panel" target="_blank" class="link">Github</a> |
<a href="https://gitee.com/hslr/sun-panel" target="_blank" class="link">Gitee</a>
<div class="flex flex-col items-center justify-center text-base">
<div>
建议反馈<a href="https://github.com/hslr-s/sun-panel/issues" target="_blank" class="link">Github Issues</a>
</div>
<div>
QQ交流群<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=_I9WIoJn1roIdoaAqelSj9qClLKlXIa1&authKey=GfsQP2GagHnus0jMc7U8Sm6VhWjtsipXUzCHbFwQsGyHMgmYWx6ZbAP%2Bhut%2B4D6N&noverify=0&group_code=276594668" target="_blank" class="link">276594668</a>
</div>
<div>
开发者<a href="https://blog.enianteam.com/u/sun/content/11" target="_blank" class="link">红烧猎人</a> | <a href="https://github.com/hslr-s/sun-panel/blob/master/doc/donate.md" target="_blank" class="text-red-600 hover:text-red-900">🧧打赏</a>
</div>
<div class="flex mt-[10px]">
<div class="flex items-center mx-[10px]">
<img class="w-[20px] h-[20px] mr-[5px]" :src="srcGithub" alt="">
<a href="https://github.com/hslr-s/sun-panel" target="_blank" class="link">Github</a>
</div>
<div class="flex items-center mx-[10px]">
<img class="w-[20px] h-[20px] mr-[5px]" :src="srcGitee" alt="">
<a href="https://gitee.com/hslr/sun-panel" target="_blank" class="link">Gitee</a>
</div>
<div class="flex items-center mx-[10px]">
<img class="w-[20px] h-[20px] mr-[5px]" :src="srcDocker" alt="">
<a href="https://hub.docker.com/r/hslr/sun-panel" target="_blank" class="link">Docker</a>
</div>
</div>
</div>
</div>
</template>
<style>
.link{
color:blue
color:rgb(0, 89, 255)
}
</style>

View File

@ -56,6 +56,12 @@ const rules: FormRules = {
type: 'number',
message: '请选择账号状态',
},
password: {
trigger: 'blur',
min: 6,
max: 20,
message: '6-20个字符',
},
}
//
@ -106,7 +112,7 @@ const handleValidateButtonClick = (e: MouseEvent) => {
</NFormItem>
<NFormItem path="password" label="密码">
<NInput v-model:value="model.password" type="text" :placeholder="`${userInfo?.id ? '请输入新密码,留空密码不变' : '请输入密码'}`" />
<NInput v-model:value="model.password" :maxlength="20" type="password" :placeholder="`${userInfo?.id ? '请输入新密码,留空密码不变' : '请输入密码'}`" />
</NFormItem>
</NForm>

View File

@ -0,0 +1,198 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { NButton, NCard, NForm, NFormItem, NInput, useDialog, useMessage } from 'naive-ui'
import { VueDraggable } from 'vue-draggable-plus'
import { deletes, edit, getList, saveSort } from '@/api/panel/itemIconGroup'
import { RoundCardModal, SvgIcon } from '@/components/common'
interface EditModalArg {
show: boolean
editStatus: number // 1. 2.
model: Panel.ItemIconGroup
rules: FormRules
}
const formRef = ref<FormInst | null>(null)
const ms = useMessage()
const dialog = useDialog()
const sortStatus = ref(false)
const defaultMNodal = {
title: '',
icon: 'material-symbols:folder-outline',
sort: 9999,
}
const editModalArg = ref<EditModalArg>({
show: false,
editStatus: 1,
model: defaultMNodal,
rules: {
title: [
{
required: true,
trigger: 'blur',
message: '必填项',
},
],
},
})
const groups = ref<Panel.ItemIconGroup[]>([])
function handleAddGroup() {
editModalArg.value.show = !editModalArg.value.show
}
function handleEditGroup(groupInfo: Panel.ItemIconGroup) {
editModalArg.value.show = true
editModalArg.value.model = groupInfo
}
function handleDragSort() {
sortStatus.value = true
}
function handleSaveSort() {
const saveItems: Common.SortItemRequest[] = []
for (let i = 0; i < groups.value.length; i++) {
const element = groups.value[i]
saveItems.push({
id: element.id as number,
sort: i + 1,
})
}
saveSort(saveItems).then(({ code, msg }) => {
if (code === 0) {
ms.success('保存成功')
sortStatus.value = false
}
else {
ms.error(`保存失败:${msg}`)
}
})
}
function handleDelete(groupInfo: Panel.ItemIconGroup) {
dialog.warning({
title: '警告',
content: `你确定删除此分组[ ${groupInfo.title} ],删除后此分组应用图标将丢失?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (groupInfo.id) {
deletes([groupInfo.id]).then(({ code, msg }) => {
if (code !== 0)
ms.error(`删除失败:${msg}`)
else
refreshList()
})
}
},
})
}
function handleSaveGroup() {
formRef.value?.validate((errors) => {
if (!errors) {
edit(editModalArg.value.model).then(({ code, msg }) => {
if (code !== 0)
ms.error(msg)
refreshList()
editModalArg.value.show = false
editModalArg.value.model = { ...defaultMNodal }
})
}
else { console.log(errors) }
})
}
function refreshList() {
getList<Common.ListResponse<Panel.ItemIconGroup[]>>().then(({ code, data }) => {
groups.value = data.list
})
}
onMounted(() => {
refreshList()
})
</script>
<template>
<div class="h-[500px]">
<div>
<NButton type="success" size="small" style="margin-right: 10px;" @click="handleAddGroup">
新增分组
</NButton>
<NButton v-if="!sortStatus" size="small" @click="handleDragSort">
排序
</NButton>
<NButton v-else type="warning" size="small" @click="handleSaveSort">
保存排序
</NButton>
</div>
<div class=" overflow-auto w-full mt-[20px] bg-slate-200 rounded-xl" style="height:calc(100% - 50px)">
<VueDraggable
v-model="groups"
item-key="sort" :animation="300"
:style="{ padding: sortStatus ? '20px' : '10px' }"
:disabled="!sortStatus"
>
<div v-for="(item, index) in groups" :key="index" class="w-full">
<NCard size="small" style="border-radius:10px;margin-bottom: 10px;">
<div class="flex" :class="sortStatus ? 'cursor-move' : ''">
<div class="flex items-center">
<span class="mr-[10px]">
<SvgIcon class="text-[20px]" icon="material-symbols:ad-group-outline" />
<!-- <SvgIcon class="text-[20px]" :icon="item.icon" /> -->
</span>
<span>
{{ item.title }}
</span>
</div>
<div class="ml-auto">
<span>
<NButton strong secondary type="success" size="small" @click="handleEditGroup(item)">
<template #icon>
<SvgIcon icon="basil:edit-solid" />
</template>
</NButton>
</span>
<span class="ml-[10px]">
<NButton strong secondary type="error" size="small" class="ml-[10px]" @click="handleDelete(item)">
<template #icon>
<SvgIcon icon="material-symbols:delete" />
</template>
</NButton>
</span>
</div>
</div>
</NCard>
</div>
</VueDraggable>
</div>
<RoundCardModal v-model:show="editModalArg.show" type="small" :title="editModalArg.editStatus === 1 ? '添加' : '编辑'" style="width: 400px;">
<NForm ref="formRef" :model="editModalArg.model" :rules="editModalArg.rules">
<NFormItem path="title" label="分组名称">
<NInput v-model:value="editModalArg.model.title" type="text" :maxlength="20" show-count placeholder="请输入" />
</NFormItem>
<!-- <NFormItem path="name" label="昵称">
<NInput v-model:value="editModalArg.model" type="text" placeholder="请输入昵称" />
</NFormItem> -->
</NForm>
<template #footer>
<NButton type="success" size="small" class="float-right" @click="handleSaveGroup">
确定
</NButton>
</template>
</RoundCardModal>
</div>
</template>

View File

@ -4,6 +4,7 @@ import type { UploadFileInfo } from 'naive-ui'
import { NButton, NCard, NColorPicker, NInput, NPopconfirm, NSelect, NSlider, NSwitch, NUpload, NUploadDragger, useMessage } from 'naive-ui'
import { useAuthStore, usePanelState } from '@/store'
import { set as setUserConfig } from '@/api/panel/userConfig'
import { PanelPanelConfigStyleEnum } from '@/enums/panel'
const authStore = useAuthStore()
const panelState = usePanelState()
@ -14,11 +15,11 @@ const isSaveing = ref(false)
const iconTypeOptions = [
{
label: '详情图标',
value: 0,
value: PanelPanelConfigStyleEnum.info,
},
{
label: '小图标',
value: 1,
value: PanelPanelConfigStyleEnum.icon,
},
]
@ -85,6 +86,68 @@ function resetPanelConfig() {
</div>
</NCard>
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
<div class="text-slate-500 mb-[5px]">
搜索框
</div>
<div class="flex items-center mt-[5px]">
<span class="mr-[10px]">显示</span>
<NSwitch v-model:value="panelState.panelConfig.searchBoxShow" />
</div>
</NCard>
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
<div class="text-slate-500 mb-[5px]">
图标
</div>
<div class="mt-[5px]">
<div>
样式
</div>
<div class="flex items-center mt-[5px]">
<NSelect v-model:value="panelState.panelConfig.iconStyle" :options="iconTypeOptions" />
</div>
</div>
<div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.info" class="mt-[5px]">
<div>
隐藏描述信息
</div>
<div class="flex items-center mt-[5px]">
<NSwitch v-model:value="panelState.panelConfig.iconTextInfoHideDescription" />
</div>
</div>
<div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.icon" class="mt-[5px]">
<div>
隐藏标题
</div>
<div class="flex items-center mt-[5px]">
<NSwitch v-model:value="panelState.panelConfig.iconTextIconHideTitle" />
</div>
</div>
<div class="mt-[5px]">
<div>
文字颜色
</div>
<div class="flex items-center mt-[5px]">
<NColorPicker
v-model:value="panelState.panelConfig.iconTextColor"
:show-alpha="false"
size="small"
:modes="['hex']"
:swatches="[
'#000000',
'#ffffff',
'#18A058',
'#2080F0',
'#F0A020',
]"
/>
</div>
</div>
</NCard>
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
<div class="text-slate-500 mb-[5px]">
壁纸
@ -99,9 +162,9 @@ function resetPanelConfig() {
:directory-dnd="true"
@finish="handleUploadBackgroundFinish"
>
<NUploadDragger>
<NUploadDragger style="width: 100%;">
<div
class="h-[150px] w-[280px] border bg-slate-100 flex justify-center items-center cursor-pointer rounded-[10px]"
class="h-[200px] w-full border bg-slate-100 flex justify-center items-center cursor-pointer rounded-[10px]"
:style="{ background: `url(${panelState.panelConfig.backgroundImageSrc}) no-repeat`, backgroundSize: 'cover' }"
>
<div class="text-shadow text-white">
@ -111,39 +174,14 @@ function resetPanelConfig() {
</NUploadDragger>
</NUpload>
<div class="flex items-center mt-[5px]">
<span class="mr-[10px]">模糊处理</span>
<div class="flex items-center mt-[10px]">
<span class="mr-[10px]">模糊</span>
<NSlider v-model:value="panelState.panelConfig.backgroundBlur" class="max-w-[200px]" :step="2" :max="20" />
</div>
</NCard>
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
<div class="text-slate-500 mb-[5px]">
图标
</div>
<div>
样式
</div>
<div class="flex items-center mt-[5px]">
<NSelect v-model:value="panelState.panelConfig.iconStyle" :options="iconTypeOptions" />
</div>
<div>
文字颜色
</div>
<div class="flex items-center mt-[5px]">
<NColorPicker
v-model:value="panelState.panelConfig.iconTextColor"
:show-alpha="false"
size="small"
:modes="['hex']"
:swatches="[
'#000000',
'#ffffff',
'#18A058',
'#2080F0',
'#F0A020',
]"
/>
<div class="flex items-center mt-[10px]">
<span class="mr-[10px]">遮罩</span>
<NSlider v-model:value="panelState.panelConfig.backgroundMaskNumber" class="max-w-[200px]" :step="0.1" :max="1" />
</div>
</NCard>

View File

@ -3,6 +3,7 @@ import { NButton, NCard, useDialog, useMessage } from 'naive-ui'
import { useAuthStore, usePanelState, useUserStore } from '@/store'
import { logout } from '@/api'
import { router } from '@/router'
import { SvgIcon } from '@/components/common/'
const userStore = useUserStore()
const authStore = useAuthStore()
@ -51,6 +52,9 @@ function handleLogiut() {
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
<NButton size="small" quaternary type="error" @click="handleLogiut">
<template #icon>
<SvgIcon icon="tabler:logout" />
</template>
退出登录
</NButton>
</NCard>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { h, onMounted, reactive, ref } from 'vue'
import { NButton, NDataTable, NDropdown, useDialog, useMessage } from 'naive-ui'
import { NAlert, NButton, NDataTable, NDropdown, useDialog, useMessage } from 'naive-ui'
import type { DataTableColumns, PaginationProps } from 'naive-ui'
import EditUser from './EditUser/index.vue'
import { deletes as usersDeletes, getList as usersGetList } from '@/api/panel/users'
@ -171,7 +171,10 @@ onMounted(() => {
<template>
<div class="h-[500px] overflow-auto">
<div class="mb-[10px]">
<NAlert type="info" :bordered="false">
账号之间的数据不互通
</NAlert>
<div class="my-[10px]">
<NButton type="primary" size="small" ghost @click="handleAdd">
添加
</NButton>

View File

@ -1,7 +1,8 @@
import Result from './Result/index.vue'
import EditItem from './EditItem/index.vue'
import Setting from './Setting/index.vue'
import AppIcon from './AppIcon/index.vue'
export {
Result, EditItem, Setting,
Result, EditItem, Setting, AppIcon,
}

View File

@ -1,20 +1,36 @@
<script setup lang="ts">
import { NButton, NButtonGroup, NDropdown, NEllipsis, NGrid, NGridItem, NModal, NSkeleton, NSpin, useDialog, useMessage } from 'naive-ui'
import { nextTick, onMounted, ref } from 'vue'
import { EditItem, Setting } from './components'
import { Clock } from '@/components/deskModule'
import { ItemIcon, SvgIcon } from '@/components/common'
import { deletes, getListByGroupId } from '@/api/panel/itemIcon'
import { VueDraggable } from 'vue-draggable-plus'
import { NBackTop, NButton, NButtonGroup, NDropdown, NModal, NSkeleton, NSpin, useDialog, useMessage } from 'naive-ui'
import { nextTick, onMounted, ref, watch } from 'vue'
import { AppIcon, EditItem, Setting } from './components'
import { Clock, SearchBox } from '@/components/deskModule'
import { SvgIcon } from '@/components/common'
import { deletes, getListByGroupId, saveSort } from '@/api/panel/itemIcon'
import { getList as getGroupList } from '@/api/panel/itemIconGroup'
import { getInfo } from '@/api/system/user'
import { usePanelState, useUserStore } from '@/store'
import { PanelStateNetworkModeEnum } from '@/enum'
import { PanelPanelConfigStyleEnum, PanelStateNetworkModeEnum } from '@/enums'
import { setTitle } from '@/utils/cmn'
interface StateDragAppSort {
status: boolean
}
interface ItemGroup extends Panel.ItemIconGroup {
items?: Panel.ItemInfo[]
}
const stateDragAppSort = ref<StateDragAppSort>({
status: false,
})
const ms = useMessage()
const dialog = useDialog()
const panelState = usePanelState()
const userStore = useUserStore()
const scrollContainerRef = ref<HTMLElement | undefined>(undefined)
const editItemInfoShow = ref<boolean>(false)
const editItemInfoData = ref<Panel.ItemInfo | null>(null)
const windowShow = ref<boolean>(false)
@ -31,27 +47,33 @@ const currentRightSelectItem = ref<Panel.ItemInfo | null>(null)
const settingModalShow = ref(false)
const dropdownMenuOptions = [
{
label: '新窗口打开',
key: 'newWindows',
},
{
label: '编辑',
key: 'edit',
},
{
label: '删除',
key: 'delete',
},
]
const items = ref<Panel.ItemInfo[]>()
const items = ref<ItemGroup[]>([])
function handleAddAppClick() {
editItemInfoData.value = null
editItemInfoShow.value = true
}
function openPage(openMethod: number, url: string, title?: string) {
switch (openMethod) {
case 1:
window.location.href = url
break
case 2:
window.open(url)
break
case 3:
windowShow.value = true
windowSrc.value = url
windowTitle.value = title || url
windowIframeIsLoad.value = true
break
default:
break
}
}
function handleItemClick(item: Panel.ItemInfo) {
let jumpUrl = ''
@ -60,23 +82,7 @@ function handleItemClick(item: Panel.ItemInfo) {
if (item.lanUrl === '')
jumpUrl = item.url
switch (item.openMethod) {
case 1:
window.location.href = jumpUrl
break
case 2:
window.open(jumpUrl)
break
case 3:
windowShow.value = true
windowSrc.value = jumpUrl
windowTitle.value = item.title
windowIframeIsLoad.value = true
break
default:
break
}
openPage(item.openMethod, jumpUrl, item.title)
}
function handWindowIframeIdLoad(payload: Event) {
@ -84,9 +90,18 @@ function handWindowIframeIdLoad(payload: Event) {
}
function getList() {
getListByGroupId<Common.ListResponse<Panel.ItemInfo[]>>().then((res) => {
if (res.code === 0)
items.value = res.data.list
//
getGroupList<Common.ListResponse<ItemGroup[]>>().then(({ code, data, msg }) => {
if (code === 0)
items.value = data.list
for (let i = 0; i < data.list.length; i++) {
const element = data.list[i]
getListByGroupId<Common.ListResponse<Panel.ItemInfo[]>>(element.id).then((res) => {
if (res.code === 0)
items.value[i].items = res.data.list
})
}
// console.log(items)
})
}
@ -100,6 +115,14 @@ function handleSelect(key: string | number) {
case 'newWindows':
window.open(jumpUrl)
break
case 'openWanUrl':
if (currentRightSelectItem.value)
openPage(currentRightSelectItem.value?.openMethod, currentRightSelectItem.value?.url, currentRightSelectItem.value?.title)
break
case 'openLanUrl':
if (currentRightSelectItem.value && currentRightSelectItem.value.lanUrl)
openPage(currentRightSelectItem.value?.openMethod, currentRightSelectItem.value.lanUrl, currentRightSelectItem.value?.title)
break
case 'edit':
// 使{...}
editItemInfoData.value = { ...currentRightSelectItem.value } as Panel.ItemInfo
@ -153,12 +176,83 @@ function handleEditSuccess(item: Panel.ItemInfo) {
function handleChangeNetwork(mode: PanelStateNetworkModeEnum) {
panelState.setNetworkMode(mode)
if (mode === PanelStateNetworkModeEnum.lan)
ms.success('已经切换成局域网模式,此时再点击已填写局域网地址的图标将跳转至局域网地址(此配置仅保存在本地)')
ms.success('已经切换成局域网模式(此配置仅保存在本地)')
else
ms.success('已经切换成互联网模式(此配置仅保存在本地)')
}
//
function handleEndDrag(event: any, itemIconGroup: Panel.ItemIconGroup) {
// console.log(event)
// console.log(items.value)
}
function handleSaveSort(itemGroup: ItemGroup) {
const saveItems: Common.SortItemRequest[] = []
if (itemGroup.items) {
for (let i = 0; i < itemGroup.items.length; i++) {
const element = itemGroup.items[i]
saveItems.push({
id: element.id as number,
sort: i + 1,
})
}
saveSort({ itemIconGroupId: itemGroup.id as number, sortItems: saveItems }).then(({ code, msg }) => {
if (code === 0) {
//
ms.success('保存成功')
// sortStatus.value = false
}
else {
ms.error(`保存失败:${msg}`)
}
})
}
}
function getDropdownMenuOptions() {
const dropdownMenuOptions = [
{
label: '新窗口打开',
key: 'newWindows',
},
]
if (currentRightSelectItem.value?.lanUrl && panelState.networkMode === PanelStateNetworkModeEnum.wan) {
dropdownMenuOptions.push({
label: '打开局域网地址',
key: 'openLanUrl',
})
}
if (currentRightSelectItem.value?.lanUrl && panelState.networkMode === PanelStateNetworkModeEnum.lan) {
dropdownMenuOptions.push({
label: '打开互联网地址',
key: 'openWanUrl',
})
}
dropdownMenuOptions.push({
label: '编辑',
key: 'edit',
}, {
label: '删除',
key: 'delete',
})
return dropdownMenuOptions
}
watch(() => stateDragAppSort.value.status, (newvalue: boolean) => {
if (newvalue === false)
getList()
else
ms.warning('进入排序模式,记得点击保存再退出')
})
onMounted(() => {
getList()
@ -180,120 +274,137 @@ onMounted(() => {
<template>
<div class="w-full h-full sun-main ">
<div
class="cover"
:style="{
class="cover" :style="{
filter: `blur(${panelState.panelConfig.backgroundBlur}px)`,
background: `url(${panelState.panelConfig.backgroundImageSrc}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
<div class="absolute w-full h-full overflow-auto">
<div class="mask" :style="{ backgroundColor: `rgba(0,0,0,${panelState.panelConfig.backgroundMaskNumber})` }" />
<div ref="scrollContainerRef" class="absolute w-full h-full overflow-auto">
<div class="p-2.5 max-w-[1200px] mx-auto mt-[10%]">
<!-- -->
<div class="mx-[auto] w-[80%]">
<div class="flex mx-[auto] items-center justify-center text-white">
<div>
<span class="text-5xl font-bold text-shadow">
<span class="text-2xl md:text-5xl font-bold text-shadow">
{{ panelState.panelConfig.logoText }}
</span>
</div>
<div class="text-2xl mx-[10px]">
<div class="text-base lg:text-2xl mx-[10px]">
|
</div>
<div class="text-shadow">
<Clock :hide-second="!panelState.panelConfig.clockShowSecond" />
</div>
</div>
<!-- <div class="flex mt-[20px] mx-auto w-[80%]">
<SearchBox />
</div> -->
<div v-if="panelState.panelConfig.searchBoxShow" class="flex mt-[20px] mx-auto sm:w-full lg:w-[80%]">
<SearchBox />
</div>
</div>
<!-- 图标 -->
<!-- 应用盒子 -->
<div class="mt-[50px]">
<!-- 详情图标 -->
<div v-if="panelState.panelConfig.iconStyle === 0">
<NGrid :x-gap="15" :y-gap="15" item-responsive cols="1 200:1 400:2 600:3 800:4 1000:5 1200:6">
<NGridItem v-for="(item, index) in items" :key="index">
<div @contextmenu="(e) => handleContextMenu(e, item)">
<div
class="w-full rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] bg-[#2a2a2a6b] flex"
@click="handleItemClick(item)"
>
<div class="w-[70px]">
<ItemIcon :item-icon="item.icon" />
</div>
<div class="text-white m-[8px_8px_0_8px]" :style="{ color: panelState.panelConfig.iconTextColor }">
<div>
<NEllipsis>
{{ item.title }}
</NEllipsis>
</div>
<div>
<NEllipsis :line-clamp="2" class="text-xs">
{{ item.description }}
</NEllipsis>
</div>
</div>
</div>
</div>
</NGridItem>
<!-- 组纵向排列 -->
<div
v-for="(itemGroup, itemGroupIndex) in items"
:key="itemGroupIndex"
class="mt-[50px]"
:class="stateDragAppSort.status ? 'shadow-2xl border shadow-[0_0_30px_10px_rgba(0,0,0,0.8)] p-[10px] rounded-2xl' : ''"
>
<!-- 分组标题 -->
<div class="text-white text-xl font-extrabold mb-[20px] ml-[10px]">
{{ itemGroup.title }}
</div>
<NGridItem>
<div>
<div
class="w-full rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] bg-[#2a2a2a6b] flex"
@click="handleAddAppClick"
>
<ItemIcon :item-icon="{ itemType: 3, text: 'subway:add', bgColor: '#00000000' }" />
<div class="text-white m-[8px]" :style="{ color: panelState.panelConfig.iconTextColor }">
<div>
<NEllipsis>
添加图标
</NEllipsis>
</div>
<!-- 详情图标 -->
<div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.info">
<div v-if="itemGroup.items">
<VueDraggable
v-model="itemGroup.items" item-key="sort" :animation="300"
class="mx-auto mt-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:12 gap-5"
filter=".not-drag"
:disabled="!stateDragAppSort.status"
@end="(event) => handleEndDrag(event, itemGroup)"
>
<div v-for="item, index in itemGroup.items" :key="index" :title="item.description" @contextmenu="(e) => handleContextMenu(e, item)">
<AppIcon
:class="stateDragAppSort.status ? 'cursor-move' : 'cursor-pointer'"
:item-info="item"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="panelState.panelConfig.iconTextInfoHideDescription || false"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="0"
@click="handleItemClick(item)"
/>
</div>
<div class="text text-xs">
<NEllipsis>
新增一个新的图标
</NEllipsis>
</div>
</div>
<div v-if="itemGroup.items.length === 0" class="not-drag">
<AppIcon
:class="stateDragAppSort.status ? 'cursor-move' : 'cursor-pointer'"
:item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: '添加图标', url: '', openMethod: 0 }"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="panelState.panelConfig.iconTextInfoHideDescription || false"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="0"
@click="handleAddAppClick"
/>
</div>
</div>
</NGridItem>
</NGrid>
</div>
</VueDraggable>
</div>
</div>
<!-- APP图标宫型盒子 -->
<div v-if="panelState.panelConfig.iconStyle === 1">
<NGrid :x-gap="12" :y-gap="8" item-responsive cols="3 300:4 600:6 900:8">
<NGridItem v-for="(item, index) in items" :key="index">
<div @contextmenu="(e) => handleContextMenu(e, item)">
<div
class="w-[70px] h-[70px] mx-auto rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] bg-[#2a2a2a6b]"
@click="handleItemClick(item)"
>
<ItemIcon :item-icon="item.icon" />
</div>
<div class="text-center app-icon-text-shadow cursor-pointer mt-[2px]" :style="{ color: panelState.panelConfig.iconTextColor }" @click="handleItemClick(item)">
<span>{{ item.title }}</span>
</div>
</div>
</NGridItem>
<!-- APP图标宫型盒子 -->
<div v-if="panelState.panelConfig.iconStyle === PanelPanelConfigStyleEnum.icon">
<div v-if="itemGroup.items">
<VueDraggable
v-model="itemGroup.items" item-key="id" :animation="300"
class="mx-auto mt-4 grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 xl:12 gap-5"
<NGridItem>
<div>
<div class="w-[70px] h-[70px] mx-auto rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)]" @click="handleAddAppClick">
<ItemIcon :item-icon="{ itemType: 3, text: 'subway:add', bgColor: '#343434' }" />
filter=".not-drag"
:disabled="!stateDragAppSort.status"
>
<div v-for="item, index in itemGroup.items" :key="index" :title="item.description" @contextmenu="(e) => handleContextMenu(e, item)">
<AppIcon
:class="stateDragAppSort.status ? 'cursor-move' : 'cursor-pointer'"
:item-info="item"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="!panelState.panelConfig.iconTextInfoHideDescription"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="1"
@click="handleItemClick(item)"
/>
</div>
<div class="text-center app-icon-text-shadow cursor-pointer mt-[2px]" :style="{ color: panelState.panelConfig.iconTextColor }" @click="handleAddAppClick">
添加图标
<div v-if="itemGroup.items.length === 0" class="not-drag">
<AppIcon
:class="stateDragAppSort.status ? 'cursor-move' : 'cursor-pointer'"
:item-info="{ icon: { itemType: 3, text: 'subway:add' }, title: '添加图标', url: '', openMethod: 0 }"
:icon-text-color="panelState.panelConfig.iconTextColor"
:icon-text-info-hide-description="!panelState.panelConfig.iconTextInfoHideDescription"
:icon-text-icon-hide-title="panelState.panelConfig.iconTextIconHideTitle || false"
:style="1"
@click="handleAddAppClick"
/>
</div>
</div>
</NGridItem>
</NGrid>
</vuedraggable>
</div>
</div>
<!-- 编辑栏 -->
<div v-if="stateDragAppSort.status" class="flex mt-[10px]">
<div>
<NButton color="#2a2a2a6b" @click="handleSaveSort(itemGroup)">
<template #icon>
<SvgIcon class="text-white font-xl" icon="material-symbols:save" />
</template>
<div>
保存排序
</div>
</NButton>
</div>
</div>
</div>
</div>
</div>
@ -301,29 +412,48 @@ onMounted(() => {
<!-- 右键菜单 -->
<NDropdown
placement="bottom-start"
trigger="manual"
:x="dropdownMenuX"
:y="dropdownMenuY"
:options="dropdownMenuOptions"
:show="dropdownShow"
:on-clickoutside="onClickoutside"
@select="handleSelect"
placement="bottom-start" trigger="manual" :x="dropdownMenuX" :y="dropdownMenuY"
:options="getDropdownMenuOptions()" :show="dropdownShow" :on-clickoutside="onClickoutside" @select="handleSelect"
/>
<!-- 悬浮按钮 -->
<div class="fixed-element shadow-[0_0_10px_2px_rgba(0,0,0,0.2)]">
<NButtonGroup vertical>
<NButton v-if="panelState.networkMode === PanelStateNetworkModeEnum.lan" color="#2a2a2a6b" title="当前:局域网模式,点击切换成互联网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.wan)">
<NButton v-if="stateDragAppSort.status" color="#2a2a2a6b" @click="stateDragAppSort.status = !stateDragAppSort.status">
<template #icon>
<SvgIcon class="text-white font-xl" icon="ri:drag-drop-line" />
</template>
</NButton>
<NButtonGroup v-if="!stateDragAppSort.status" vertical>
<NButton color="#2a2a2a6b" @click="handleAddAppClick">
<template #icon>
<SvgIcon class="text-white font-xl" icon="typcn:plus" />
</template>
</NButton>
<NButton
v-if="panelState.networkMode === PanelStateNetworkModeEnum.lan" color="#2a2a2a6b"
title="当前:局域网模式,点击切换成互联网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.wan)"
>
<template #icon>
<SvgIcon class="text-white font-xl" icon="material-symbols:lan-outline" />
</template>
</NButton>
<NButton v-if="panelState.networkMode === PanelStateNetworkModeEnum.wan" color="#2a2a2a6b" title="当前:互联网模式,点击切换成局域网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.lan)">
<NButton
v-if="panelState.networkMode === PanelStateNetworkModeEnum.wan" color="#2a2a2a6b"
title="当前:互联网模式,点击切换成局域网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.lan)"
>
<template #icon>
<SvgIcon class="text-white font-xl" icon="mdi:wan" />
</template>
</NButton>
<NButton color="#2a2a2a6b" title="排序模式" @click="stateDragAppSort.status = !stateDragAppSort.status">
<template #icon>
<SvgIcon class="text-white font-xl" icon="ri:drag-drop-line" />
</template>
</NButton>
<NButton color="#2a2a2a6b" @click="settingModalShow = !settingModalShow">
<template #icon>
<SvgIcon class="text-white font-xl" icon="ep:setting" />
@ -331,20 +461,30 @@ onMounted(() => {
</NButton>
</NButtonGroup>
<NBackTop
:listen-to="() => scrollContainerRef"
:right="10"
:bottom="10"
style="background-color:transparent;border: none;box-shadow: none;"
>
<div class="shadow-[0_0_10px_2px_rgba(0,0,0,0.2)]">
<NButton color="#2a2a2a6b">
<template #icon>
<SvgIcon class="text-white font-xl" icon="icon-park-outline:to-top" />
</template>
</NButton>
</div>
</NBackTop>
<Setting v-model:visible="settingModalShow" />
</div>
<EditItem v-model:visible="editItemInfoShow" :item-info="editItemInfoData" @done="handleEditSuccess" />
<!-- 新窗口 -->
<!-- 弹窗 -->
<NModal
v-model:show="windowShow"
:mask-closable="false"
preset="card"
style="max-width: 1000px;height: 600px;border-radius: 1rem;"
:bordered="false"
size="small"
role="dialog"
v-model:show="windowShow" :mask-closable="false" preset="card"
style="max-width: 1000px;height: 600px;border-radius: 1rem;" :bordered="false" size="small" role="dialog"
aria-modal="true"
>
<template #header>
@ -358,45 +498,60 @@ onMounted(() => {
</template>
<div class="w-full h-full rounded-2xl overflow-hidden border">
<NSkeleton v-if="windowIframeIsLoad" height="100%" width="100%" />
<iframe v-show="!windowIframeIsLoad" id="windowIframeId" ref="windowIframeRef" :src="windowSrc" class="w-full h-full" frameborder="0" @load="handWindowIframeIdLoad" />
<iframe
v-show="!windowIframeIsLoad" id="windowIframeId" ref="windowIframeRef" :src="windowSrc"
class="w-full h-full" frameborder="0" @load="handWindowIframeIdLoad"
/>
</div>
</NModal>
</div>
</template>
<style>
body,html{
body,
html {
overflow: hidden;
background-color: rgb(54, 54, 54);
}
</style>
<style scoped>
.sun-main{
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.sun-main {
user-select: none;
}
.cover{
position:absolute;
width:100%;
height:100%;
.cover {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
/* background: url(@/assets/start_sky.jpg) no-repeat; */
transform: scale(1.05);
}
.text-shadow{
.text-shadow {
text-shadow: 2px 2px 50px rgb(0, 0, 0);
}
.app-icon-text-shadow{
.app-icon-text-shadow {
text-shadow: 2px 2px 5px rgb(0, 0, 0);
}
.fixed-element {
position: fixed; /* 将元素固定在屏幕上 */
right: 30px; /* 距离屏幕顶部的距离 */
bottom: 50px; /* 距离屏幕左侧的距离 */
position: fixed;
/* 将元素固定在屏幕上 */
right: 10px;
/* 距离屏幕顶部的距离 */
bottom: 50px;
/* 距离屏幕左侧的距离 */
}
</style>

View File

@ -10,7 +10,7 @@ const userStore = useUserStore()
const authStore = useAuthStore()
const ms = useMessage()
const isShowCaptcha = ref<boolean>(false)
const isShowRegister = ref<boolean>(false)
// const isShowRegister = ref<boolean>(false)
const captchaRef = ref()
@ -42,13 +42,13 @@ function handleSubmit() {
<template>
<div class="login-container">
<NCard class="login-card">
<div class="login-title">
<NGradientText :size="30" type="success">
<NCard class="login-card" style="border-radius: 20px;">
<div class="login-title ">
<NGradientText :size="30" type="success" class="!font-bold">
{{ $t('common.appName') }}
</NGradientText>
</div>
<NForm :model="form" label-width="100px">
<NForm :model="form" label-width="100px" @keydown.enter="handleSubmit">
<NFormItem>
<NInput v-model:value="form.username" placeholder="请输入邮箱地址作为账号">
<template #prefix>
@ -77,13 +77,17 @@ function handleSubmit() {
</NButton>
</NFormItem>
<div class="flex justify-end">
<!-- <div class="flex justify-end">
<NButton v-if="isShowRegister" quaternary type="info" class="flex" @click="$router.push({ path: '/register' })">
注册
</NButton>
<!-- <NButton quaternary type="info" class="flex" @click="$router.push({ path: '/resetPassword' })">
<NButton quaternary type="info" class="flex" @click="$router.push({ path: '/resetPassword' })">
忘记密码?
</NButton> -->
</NButton>
</div> -->
<div class="flex justify-center text-slate-300">
Powered By <a href="https://github.com/hslr-s/sun-panel" target="_blank" class="ml-[5px] text-slate-500">Sun-Panel</a>
</div>
</NForm>
</NCard>