2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: v2 Initial commit

This commit is contained in:
耗子
2023-06-22 00:09:56 +08:00
commit c14f4de880
239 changed files with 48925 additions and 0 deletions

32
.air.toml Normal file
View File

@@ -0,0 +1,32 @@
root = "."
tmp_dir = "storage/temp"
[build]
bin = "./storage/temp/main"
cmd = "go build -o ./storage/temp/main ."
delay = 1000
exclude_dir = ["storage", "database"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
APP_ENV=local
APP_KEY=
APP_DEBUG=true
JWT_SECRET=

72
.gitignore vendored Normal file
View File

@@ -0,0 +1,72 @@
tmp
.env
.history
# Golang #
# `go test -c` 生成的二进制文件
*.test
# go coverage 工具
*.out
*.prof
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
# 编译文件 #
*.com
*.class
*.dll
*.exe
*.o
*.so
# 压缩包 #
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# 日志文件和数据库 #
*.log
*.sqlite
*.db
# 临时文件 #
tmp/
.tmp/
# 系统生成文件 #
.DS_Store
.DS_Store?
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
.TemporaryItems
.fseventsd
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# IDE 和编辑器 #
.idea/
/go_build_*
out/
.vscode/
.vscode/settings.json
*.sublime*
__debug_bin
.project
# 前端工具链 #
.sass-cache/*
node_modules/

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2022] [HaoZi Technology Co., Ltd.]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

62
README.md Normal file
View File

@@ -0,0 +1,62 @@
# 耗子Linux面板
这是耗子Linux面板的开源仓库基于Apache License 2.0协议进行开源,目前处于积极开发状态。
#### 我们正在进行基于 Golang 的 V2 版本的开发,加入我们的 交流QQ群[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道[耗子](https://pd.qq.com/s/fyol46wfy) 获取最新动态。
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)
## 运行环境
面板仅支持主流 `amd64` | `arm64` 系统的最新版本,且随着系统版本的更新,面板也会逐步停止对旧版本的支持。
不支持 `Ubuntu`,因为其发版太频繁,难以维护。
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `RockyLinux` | `AlmaLinux`,维护周期更长。
不在下表中的其他 `RHEL` 系统,可自行尝试安装,但不保证能够正常运行,且不提供技术支持。
| 系统 | 版本 |
|------------|----|
| RockyLinux | 9 |
| AlmaLinux | 9 |
| Debian | 12 |
## 安装面板
```shell
TODO
```
## 日常维护
```shell
TODO
```
## 问题反馈
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forum) 提问寻求帮助。
对于面板自身问题,可在 GitHub 的`Issues`
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
交流QQ群[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道[耗子](https://pd.qq.com/s/fyol46wfy)
## 赞助商
### 服务器
- [盾云](https://www.ddunyun.com/)
### CDN
- [盾云CDN](http://cdn.ddunyun.com/)
- [又拍云](https://www.upyun.com/)
#### 接受云资源和资金赞助可通过QQ群咨询联系
## Star 趋势
[![Star 趋势](https://starchart.cc/HaoZi-Team/Panel.svg)](https://starchart.cc/HaoZi-Team/Panel)

View File

@@ -0,0 +1,78 @@
package commands
import (
"strconv"
"github.com/gookit/color"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/packages/helpers"
)
type Monitoring struct {
}
// Signature The name and signature of the console command.
func (receiver *Monitoring) Signature() string {
return "panel:monitoring"
}
// Description The console command description.
func (receiver *Monitoring) Description() string {
return "[面板] 系统监控"
}
// Extend The console command extend.
func (receiver *Monitoring) Extend() command.Extend {
return command.Extend{
Category: "panel",
}
}
// Handle Execute the console command.
func (receiver *Monitoring) Handle(ctx console.Context) error {
var setting models.Setting
if err := facades.Orm().Query().Where("key", "monitor").First(&setting); err != nil {
return nil
}
if setting.Value == "0" || len(setting.Value) == 0 {
return nil
}
info, err := helpers.GetMonitoringInfo()
if err != nil {
facades.Log().Errorf("[面板] 系统监控失败: %s", err.Error())
color.Redf("[面板] 系统监控失败: %s", err.Error())
return nil
}
err = facades.Orm().Query().Create(&models.Monitor{
Info: info,
})
if err != nil {
facades.Log().Errorf("[面板] 系统监控保存失败: %s", err.Error())
color.Redf("[面板] 系统监控保存失败: %s", err.Error())
return nil
}
// 删除过期数据
err = facades.Orm().Query().Where("key", "monitor_days").First(&setting)
if err != nil {
return nil
}
if setting.Value == "0" || len(setting.Value) == 0 {
return nil
}
days, err := strconv.Atoi(setting.Value)
if err != nil {
return nil
}
_, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{})
return nil
}

20
app/console/kernel.go Normal file
View File

@@ -0,0 +1,20 @@
package console
import (
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/schedule"
"panel/app/console/commands"
)
type Kernel struct {
}
func (kernel *Kernel) Schedule() []schedule.Event {
return []schedule.Event{}
}
func (kernel *Kernel) Commands() []console.Command {
return []console.Command{
&commands.Monitoring{},
}
}

View File

@@ -0,0 +1,94 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
models "panel/app/Models"
"panel/app/http/requests"
)
type UserController struct {
//Dependent services
}
func NewUserController() *UserController {
return &UserController{
//Inject services
}
}
func (r *UserController) Login(ctx http.Context) {
var loginRequest requests.LoginRequest
errors, err := ctx.Request().ValidateRequest(&loginRequest)
if err != nil {
ctx.Response().Json(http.StatusUnprocessableEntity, http.Json{
"code": 422,
"message": err.Error(),
})
return
}
if errors != nil {
ctx.Response().Json(http.StatusUnprocessableEntity, http.Json{
"code": 422,
"message": errors.All(),
})
return
}
var user models.User
err = facades.Orm().Query().Where("username", loginRequest.Username).First(&user)
if err != nil {
facades.Log().Error("[面板][UserController] 查询用户失败 ", err)
ctx.Response().Json(http.StatusInternalServerError, http.Json{
"code": 500,
"message": "系统内部错误",
})
return
}
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": 401,
"message": "用户名或密码错误",
})
return
}
// 检查密码是否需要重新哈希
if facades.Hash().NeedsRehash(user.Password) {
// 更新密码
user.Password, err = facades.Hash().Make(loginRequest.Password)
if err != nil {
facades.Log().Error("[面板][UserController] 更新密码失败 ", err)
ctx.Response().Json(http.StatusInternalServerError, http.Json{
"code": 500,
"message": "系统内部错误",
})
return
}
}
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
if loginErr != nil {
facades.Log().Error("[面板][UserController] 登录失败 ", loginErr)
ctx.Response().Json(http.StatusInternalServerError, http.Json{
"code": 500,
"message": loginErr.Error(),
})
return
}
ctx.Response().Success().Json(http.Json{
"code": 0,
"message": "登录成功",
"data": http.Json{
"access_token": token,
},
})
}
func (r *UserController) Info(ctx http.Context) {
ctx.Response().Success().Json(http.Json{
"Hello": "Goravel",
})
}

18
app/http/kernel.go Normal file
View File

@@ -0,0 +1,18 @@
package http
import (
"github.com/goravel/framework/contracts/http"
"panel/app/http/middleware"
)
type Kernel struct {
}
// The application's global HTTP middleware stack.
// These middleware are run during every request to your application.
func (kernel Kernel) Middleware() []http.Middleware {
return []http.Middleware{
middleware.Static(),
}
}

0
app/http/middleware/.gitignore vendored Normal file
View File

View File

@@ -0,0 +1,63 @@
package middleware
import (
"errors"
"github.com/goravel/framework/auth"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
)
// Jwt 确保通过 JWT 鉴权
func Jwt() http.Middleware {
return func(ctx http.Context) {
token := ctx.Request().Header("Authorization", "")
if len(token) == 0 {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"code": 401,
"message": "未登录",
})
return
}
// JWT 鉴权
if _, err := facades.Auth().Parse(ctx, token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
token, err = facades.Auth().Refresh(ctx)
if err != nil {
// Refresh time exceeded
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
}
// 取出用户信息
var user models.User
if err := facades.Auth().User(ctx, &user); err != nil {
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"code": 403,
"message": "用户不存在",
})
return
}
ctx.WithValue("user", user)
ctx.Response().Header("Authorization", token)
ctx.Request().Next()
}
}

View File

@@ -0,0 +1,16 @@
package middleware
import (
"github.com/gin-contrib/static"
contractshttp "github.com/goravel/framework/contracts/http"
frameworkhttp "github.com/goravel/framework/http"
)
func Static() contractshttp.Middleware {
return func(ctx contractshttp.Context) {
static.Serve("/", static.LocalFile("./public", false))(ctx.(*frameworkhttp.GinContext).Instance())
ctx.Request().Next()
}
}

View File

@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type LoginRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
func (r *LoginRequest) Authorize(ctx http.Context) error {
return nil
}
func (r *LoginRequest) Rules(ctx http.Context) map[string]string {
return map[string]string{
"login": "required",
"password": "required|min_len:8",
}
}
func (r *LoginRequest) Messages(ctx http.Context) map[string]string {
return map[string]string{
"login.required": "登录名不能为空",
"password.required": "密码不能为空",
"password.min_len": "密码长度不能小于 8 位",
}
}
func (r *LoginRequest) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *LoginRequest) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}

15
app/models/cron.go Normal file
View File

@@ -0,0 +1,15 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type Cron struct {
orm.Model
Name string `gorm:"not null;unique"`
Status bool `gorm:"not null;default:false"`
Type string `gorm:"not null"`
Time string `gorm:"not null"`
Shell string `gorm:"default:null"`
Log string `gorm:"default:null"`
}

16
app/models/database.go Normal file
View File

@@ -0,0 +1,16 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type Database struct {
orm.Model
Name string `gorm:"unique;not null"`
Type string `gorm:"not null;index"`
Host string `gorm:"not null"`
Port int `gorm:"not null"`
Username string `gorm:"not null"`
Password string `gorm:"default:null"`
Remark string `gorm:"default:null"`
}

12
app/models/monitor.go Normal file
View File

@@ -0,0 +1,12 @@
package models
import (
"panel/packages/helpers"
"github.com/goravel/framework/database/orm"
)
type Monitor struct {
orm.Model
Info helpers.MonitoringInfo `gorm:"type:json;serializer:json"`
}

13
app/models/plugin.go Normal file
View File

@@ -0,0 +1,13 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type Plugin struct {
orm.Model
Slug string `gorm:"unique;not null"`
Version string `gorm:"not null"`
Show bool `gorm:"default:false;not null"`
ShowOrder int `gorm:"default:0;not null"`
}

11
app/models/setting.go Normal file
View File

@@ -0,0 +1,11 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type Setting struct {
orm.Model
Key string `gorm:"unique;not null"`
Value string `gorm:"default:null"`
}

13
app/models/task.go Normal file
View File

@@ -0,0 +1,13 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type Task struct {
orm.Model
Name string `gorm:"not null"`
Status string `gorm:"not null;default:'waiting'"`
Shell string `gorm:"default:null"`
Log string `gorm:"default:null"`
}

13
app/models/user.go Normal file
View File

@@ -0,0 +1,13 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type User struct {
orm.Model
Username string `gorm:"unique;not null"`
Password string `gorm:"not null"`
Email string `gorm:"default:null"`
orm.SoftDeletes
}

15
app/models/website.go Normal file
View File

@@ -0,0 +1,15 @@
package models
import (
"github.com/goravel/framework/database/orm"
)
type Website struct {
orm.Model
Name string `gorm:"unique;not null"`
Status bool `gorm:"default:true;not null;index"`
Path string `gorm:"not null"`
Php int `gorm:"default:0;not null;index"`
Ssl bool `gorm:"default:false;not null;index"`
Remark string `gorm:"default:null"`
}

View File

@@ -0,0 +1,16 @@
package providers
import (
"github.com/goravel/framework/contracts/foundation"
)
type AppServiceProvider struct {
}
func (receiver *AppServiceProvider) Register(app foundation.Application) {
}
func (receiver *AppServiceProvider) Boot(app foundation.Application) {
}

View File

@@ -0,0 +1,16 @@
package providers
import (
"github.com/goravel/framework/contracts/foundation"
)
type AuthServiceProvider struct {
}
func (receiver *AuthServiceProvider) Register(app foundation.Application) {
}
func (receiver *AuthServiceProvider) Boot(app foundation.Application) {
}

View File

@@ -0,0 +1,21 @@
package providers
import (
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/facades"
"panel/app/console"
)
type ConsoleServiceProvider struct {
}
func (receiver *ConsoleServiceProvider) Register(app foundation.Application) {
kernel := console.Kernel{}
facades.Schedule().Register(kernel.Schedule())
facades.Artisan().Register(kernel.Commands())
}
func (receiver *ConsoleServiceProvider) Boot(app foundation.Application) {
}

View File

@@ -0,0 +1,22 @@
package providers
import (
"github.com/goravel/framework/contracts/event"
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/facades"
)
type EventServiceProvider struct {
}
func (receiver *EventServiceProvider) Register(app foundation.Application) {
facades.Event().Register(receiver.listen())
}
func (receiver *EventServiceProvider) Boot(app foundation.Application) {
}
func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener {
return map[event.Event][]event.Listener{}
}

View File

@@ -0,0 +1,22 @@
package providers
import (
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/facades"
)
type QueueServiceProvider struct {
}
func (receiver *QueueServiceProvider) Register(app foundation.Application) {
facades.Queue().Register(receiver.Jobs())
}
func (receiver *QueueServiceProvider) Boot(app foundation.Application) {
}
func (receiver *QueueServiceProvider) Jobs() []queue.Job {
return []queue.Job{}
}

View File

@@ -0,0 +1,27 @@
package providers
import (
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/facades"
"panel/app/http"
"panel/routes"
)
type RouteServiceProvider struct {
}
func (receiver *RouteServiceProvider) Register(app foundation.Application) {
//Add HTTP middlewares
facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...)
}
func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
receiver.configureRateLimiting()
routes.Web()
}
func (receiver *RouteServiceProvider) configureRateLimiting() {
}

View File

@@ -0,0 +1,24 @@
package providers
import (
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/contracts/validation"
"github.com/goravel/framework/facades"
)
type ValidationServiceProvider struct {
}
func (receiver *ValidationServiceProvider) Register(app foundation.Application) {
}
func (receiver *ValidationServiceProvider) Boot(app foundation.Application) {
if err := facades.Validation().AddRules(receiver.rules()); err != nil {
facades.Log().Errorf("add rules error: %+v", err)
}
}
func (receiver *ValidationServiceProvider) rules() []validation.Rule {
return []validation.Rule{}
}

17
bootstrap/app.go Normal file
View File

@@ -0,0 +1,17 @@
package bootstrap
import (
"github.com/goravel/framework/foundation"
"panel/config"
)
func Boot() {
app := foundation.NewApplication()
//Bootstrap the application
app.Boot()
//Bootstrap the config.
config.Boot()
}

7
bootstrap/plugins.go Normal file
View File

@@ -0,0 +1,7 @@
package bootstrap
import "panel/plugins/openresty"
func Plugins() {
openresty.Boot()
}

90
config/app.go Normal file
View File

@@ -0,0 +1,90 @@
package config
import (
"github.com/goravel/framework/auth"
"github.com/goravel/framework/cache"
"github.com/goravel/framework/console"
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/crypt"
"github.com/goravel/framework/database"
"github.com/goravel/framework/event"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/hash"
"github.com/goravel/framework/http"
"github.com/goravel/framework/log"
"github.com/goravel/framework/mail"
"github.com/goravel/framework/queue"
"github.com/goravel/framework/route"
"github.com/goravel/framework/schedule"
"github.com/goravel/framework/support/carbon"
"github.com/goravel/framework/validation"
"panel/app/providers"
)
// Boot Start all init methods of the current folder to bootstrap all config.
func Boot() {}
func init() {
config := facades.Config()
config.Add("app", map[string]any{
// Application Name
//
// This value is the name of your application. This value is used when the
// framework needs to place the application's name in a notification or
// any other location as required by the application or its packages.
"name": "Panel",
// Application Environment
//
// This value determines the "environment" your application is currently
// running in. This may determine how you prefer to configure various
// services the application utilizes. Set this in your ".env" file.
"env": config.Env("APP_ENV", "production"),
// Application Debug Mode
"debug": config.Env("APP_DEBUG", false),
// Application Timezone
//
// Here you may specify the default timezone for your application.
// Example: UTC, Asia/Shanghai
// More: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
"timezone": carbon.UTC,
// Encryption Key
//
// 32 character string, otherwise these encrypted strings
// will not be safe. Please do this before deploying an application!
"key": config.Env("APP_KEY", ""),
// Autoload service providers
//
// The service providers listed here will be automatically loaded on the
// request to your application. Feel free to add your own services to
// this array to grant expanded functionality to your applications.
"providers": []foundation.ServiceProvider{
&log.ServiceProvider{},
&console.ServiceProvider{},
&database.ServiceProvider{},
&cache.ServiceProvider{},
&http.ServiceProvider{},
&route.ServiceProvider{},
&schedule.ServiceProvider{},
&event.ServiceProvider{},
&queue.ServiceProvider{},
&mail.ServiceProvider{},
&auth.ServiceProvider{},
&hash.ServiceProvider{},
&crypt.ServiceProvider{},
&validation.ServiceProvider{},
&providers.AppServiceProvider{},
&providers.AuthServiceProvider{},
&providers.RouteServiceProvider{},
&providers.ConsoleServiceProvider{},
&providers.QueueServiceProvider{},
&providers.EventServiceProvider{},
&providers.ValidationServiceProvider{},
},
})
}

36
config/auth.go Normal file
View File

@@ -0,0 +1,36 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("auth", map[string]any{
// Authentication Defaults
//
// This option controls the default authentication "guard"
// reset options for your application. You may change these defaults
// as required, but they're a perfect start for most applications.
"defaults": map[string]any{
"guard": "user",
},
// Authentication Guards
//
// Next, you may define every authentication guard for your application.
// Of course, a great default configuration has been defined for you
// here which uses session storage and the Eloquent user provider.
//
// All authentication drivers have a user provider. This defines how the
// users are actually retrieved out of your database or other storage
// mechanisms used by this application to persist your user's data.
//
// Supported: "jwt"
"guards": map[string]any{
"user": map[string]any{
"driver": "jwt",
},
},
})
}

37
config/cache.go Normal file
View File

@@ -0,0 +1,37 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("cache", map[string]any{
// Default Cache Store
//
// This option controls the default cache connection that gets used while
// using this caching library. This connection is used when another is
// not explicitly specified when executing a given caching function.
"default": config.Env("CACHE_STORE", "memory"),
// Cache Stores
//
// Here you may define all the cache "stores" for your application as
// well as their drivers. You may even define multiple stores for the
// same cache driver to group types of items stored in your caches.
// Available Drivers: "memory", "redis", "custom"
"stores": map[string]any{
"memory": map[string]any{
"driver": "memory",
},
},
// Cache Key Prefix
//
// When utilizing a RAM based store such as APC or Memcached, there might
// be other applications utilizing the same cache. So, we'll specify a
// value to get prefixed to all our keys, so we can avoid collisions.
// Must: a-zA-Z0-9_-
"prefix": "panel_cache",
})
}

24
config/cors.go Normal file
View File

@@ -0,0 +1,24 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("cors", map[string]any{
// Cross-Origin Resource Sharing (CORS) Configuration
//
// Here you may configure your settings for cross-origin resource sharing
// or "CORS". This determines what cross-origin operations may execute
// in web browsers. You are free to adjust these settings as needed.
//
// To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
"allowed_methods": []string{"*"},
"allowed_origins": []string{"*"},
"allowed_headers": []string{"*"},
"exposed_headers": []string{"*"},
"max_age": 0,
"supports_credentials": false,
})
}

78
config/database.go Normal file
View File

@@ -0,0 +1,78 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("database", map[string]any{
// Default database connection name
"default": "panel",
// Database connections
"connections": map[string]any{
"panel": map[string]any{
"driver": "sqlite",
"database": "database/panel.db",
"prefix": "",
"singular": false, // Table name is singular
},
},
// Set pool configuration
"pool": map[string]any{
// Sets the maximum number of connections in the idle
// connection pool.
//
// If MaxOpenConns is greater than 0 but less than the new MaxIdleConns,
// then the new MaxIdleConns will be reduced to match the MaxOpenConns limit.
//
// If n <= 0, no idle connections are retained.
"max_idle_conns": 10,
// Sets the maximum number of open connections to the database.
//
// If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than
// MaxIdleConns, then MaxIdleConns will be reduced to match the new
// MaxOpenConns limit.
//
// If n <= 0, then there is no limit on the number of open connections.
"max_open_conns": 100,
// Sets the maximum amount of time a connection may be idle.
//
// Expired connections may be closed lazily before reuse.
//
// If d <= 0, connections are not closed due to a connection's idle time.
// Unit: Second
"conn_max_idletime": 3600,
// Sets the maximum amount of time a connection may be reused.
//
// Expired connections may be closed lazily before reuse.
//
// If d <= 0, connections are not closed due to a connection's age.
// Unit: Second
"conn_max_lifetime": 3600,
},
// Migration Repository Table
//
// This table keeps track of all the migrations that have already run for
// your application. Using this information, we can determine which of
// the migrations on disk haven't actually been run in the database.
"migrations": "migrations",
// Redis Databases
//
// Redis is an open source, fast, and advanced key-value store that also
// provides a richer body of commands than a typical key-value system
// such as APC or Memcached.
"redis": map[string]any{
"default": map[string]any{
"host": config.Env("REDIS_HOST", ""),
"password": config.Env("REDIS_PASSWORD", ""),
"port": config.Env("REDIS_PORT", 6379),
"database": config.Env("REDIS_DB", 0),
},
},
})
}

32
config/filesystems.go Normal file
View File

@@ -0,0 +1,32 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("filesystems", map[string]any{
// Default Filesystem Disk
//
// Here you may specify the default filesystem disk that should be used
// by the framework. The "local" disk, as well as a variety of cloud
// based disks are available to your application. Just store away!
"default": config.Env("FILESYSTEM_DISK", "local"),
// Filesystem Disks
//
// Here you may configure as many filesystem "disks" as you wish, and you
// may even configure multiple disks of the same driver. Defaults have
// been set up for each driver as an example of the required values.
//
// Supported Drivers: "local", "s3", "oss", "cos", "minio", "custom"
"disks": map[string]any{
"local": map[string]any{
"driver": "local",
"root": "storage/app",
"url": "http://localhost/storage",
},
},
})
}

40
config/hashing.go Normal file
View File

@@ -0,0 +1,40 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("hashing", map[string]any{
// Hashing Driver
//
// This option controls the default diver that gets used
// by the framework hash facade.
// Default driver is "bcrypt".
//
// Supported Drivers: "bcrypt", "argon2id"
"driver": "bcrypt",
// Bcrypt Hashing Options
// rounds: The cost factor that should be used to compute the bcrypt hash.
// The cost factor controls how much time is needed to compute a single bcrypt hash.
// The higher the cost factor, the more hashing rounds are done. Increasing the cost
// factor by 1 doubles the necessary time. After a certain point, the returns on
// hashing time versus attacker time are diminishing, so choose your cost factor wisely.
"bcrypt": map[string]any{
"rounds": 10,
},
// Argon2id Hashing Options
// memory: A memory cost, which defines the memory usage, given in kibibytes.
// time: A time cost, which defines the amount of computation
// realized and therefore the execution time, given in number of iterations.
// threads: A parallelism degree, which defines the number of parallel threads.
"argon2id": map[string]any{
"memory": 65536,
"time": 4,
"threads": 1,
},
})
}

31
config/http.go Normal file
View File

@@ -0,0 +1,31 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("http", map[string]any{
// HTTP URL
"url": "http://localhost",
// HTTP Host
"host": "0.0.0.0",
// HTTP Port
"port": "8888",
// HTTPS Configuration
"tls": map[string]any{
// HTTPS Host
"host": "0.0.0.0",
// HTTPS Port
"port": "8899",
// SSL Certificate
"ssl": map[string]any{
// ca.pem
"cert": "",
// ca.key
"key": "",
},
},
})
}

41
config/jwt.go Normal file
View File

@@ -0,0 +1,41 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("jwt", map[string]any{
// JWT Authentication Secret
//
// Don't forget to set this in your .env file, as it will be used to sign
// your tokens. A helper command is provided for this:
// `go run . artisan jwt:secret`
"secret": config.Env("JWT_SECRET", ""),
// JWT time to live
//
// Specify the length of time (in minutes) that the token will be valid for.
// Defaults to 1 hour.
//
// You can also set this to 0, to yield a never expiring token.
// Some people may want this behaviour for e.g. a mobile app.
// This is not particularly recommended, so make sure you have appropriate
// systems in place to revoke the token if necessary.
"ttl": config.Env("JWT_TTL", 60),
// Refresh time to live
//
// Specify the length of time (in minutes) that the token can be refreshed
// within. I.E. The user can refresh their token within a 2 week window of
// the original token being created until they must re-authenticate.
// Defaults to 2 weeks.
//
// You can also set this to null, to yield an infinite refresh time.
// Some may want this instead of never expiring tokens for e.g. a mobile app.
// This is not particularly recommended, so make sure you have appropriate
// systems in place to revoke the token if necessary.
"refresh_ttl": config.Env("JWT_REFRESH_TTL", 20160),
})
}

42
config/logging.go Normal file
View File

@@ -0,0 +1,42 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("logging", map[string]any{
// Default Log Channel
//
// This option defines the default log channel that gets used when writing
// messages to the logs. The name specified in this option should match
// one of the channels defined in the "channels" configuration array.
"default": "stack",
// Log Channels
//
// Here you may configure the log channels for your application.
// Available Drivers: "single", "daily", "custom", "stack"
// Available Level: "debug", "info", "warning", "error", "fatal", "panic"
"channels": map[string]any{
"stack": map[string]any{
"driver": "stack",
"channels": []string{"daily"},
},
"single": map[string]any{
"driver": "single",
"path": "storage/logs/goravel.log",
"level": "info",
"print": true,
},
"daily": map[string]any{
"driver": "daily",
"path": "storage/logs/goravel.log",
"level": "info",
"days": 7,
"print": true,
},
},
})
}

43
config/mail.go Normal file
View File

@@ -0,0 +1,43 @@
package config
import "github.com/goravel/framework/facades"
func init() {
config := facades.Config()
config.Add("mail", map[string]any{
// SMTP Host Address
//
// Here you may provide the host address of the SMTP server used by your
// applications. A default option is provided that is compatible with
// the Mailgun mail service which will provide reliable deliveries.
"host": config.Env("MAIL_HOST", ""),
// SMTP Host Port
//
// This is the SMTP port used by your application to deliver e-mails to
// users of the application. Like the host we have set this value to
// stay compatible with the Mailgun e-mail application by default.
"port": config.Env("MAIL_PORT", 587),
// --------------------------------------------------------------------------
// Global "From" Address
// --------------------------------------------------------------------------
//
// You may wish for all e-mails sent by your application to be sent from
// the same address. Here, you may specify a name and address that is
// used globally for all e-mails that are sent by your application.
"from": map[string]any{
"address": config.Env("MAIL_FROM_ADDRESS", "hello@example.com"),
"name": config.Env("MAIL_FROM_NAME", "Example"),
},
// SMTP Server Username
//
// If your SMTP server requires a username for authentication, you should
// set it here. This will get used to authenticate with your server on
// connection. You may also set the "password" value below this one.
"username": config.Env("MAIL_USERNAME"),
"password": config.Env("MAIL_PASSWORD"),
})
}

28
config/queue.go Normal file
View File

@@ -0,0 +1,28 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("queue", map[string]any{
// Default Queue Connection Name
"default": config.Env("QUEUE_CONNECTION", "sync"),
// Queue Connections
//
// Here you may configure the connection information for each server that is used by your application.
// Drivers: "sync", "redis"
"connections": map[string]any{
"sync": map[string]any{
"driver": "sync",
},
"redis": map[string]any{
"driver": "redis",
"connection": "default",
"queue": config.Env("REDIS_QUEUE", "default"),
},
},
})
}

0
database/migrations/.gitignore vendored Normal file
View File

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS users;

View File

@@ -0,0 +1,11 @@
CREATE TABLE users
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
username varchar(255) NOT NULL,
password varchar(255) NOT NULL,
email varchar(255) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE UNIQUE INDEX users_username_unique ON users (username);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS websites;

View File

@@ -0,0 +1,17 @@
CREATE TABLE websites
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(255) NOT NULL,
status boolean DEFAULT 1 NOT NULL,
path varchar(255) NOT NULL,
php integer DEFAULT 0 NOT NULL,
ssl boolean DEFAULT 0 NOT NULL,
remark text DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE UNIQUE INDEX websites_name_unique ON websites (name);
CREATE INDEX websites_status_index ON websites (status);
CREATE INDEX websites_php_index ON websites (php);
CREATE INDEX websites_ssl_index ON websites (ssl);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS settings;

View File

@@ -0,0 +1,10 @@
CREATE TABLE settings
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
key varchar(255) NOT NULL,
value varchar(255) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE UNIQUE INDEX settings_key_unique ON settings (key);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS databases;

View File

@@ -0,0 +1,16 @@
CREATE TABLE databases
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(255) NOT NULL,
type varchar(255) NOT NULL,
host varchar(255) NOT NULL,
port integer NOT NULL,
username varchar(255) NOT NULL,
password varchar(255) DEFAULT NULL,
remark text DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE UNIQUE INDEX databases_name_unique ON databases (name);
CREATE INDEX databases_type_index ON databases (type);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS plugins;

View File

@@ -0,0 +1,14 @@
CREATE TABLE plugins
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
slug varchar(255) NOT NULL,
version varchar(255) NOT NULL,
show boolean DEFAULT 0 NOT NULL,
show_order integer DEFAULT 0 NOT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE UNIQUE INDEX plugins_slug_unique ON plugins (slug);
CREATE INDEX plugins_show_index ON plugins (show);
CREATE INDEX plugins_show_order_index ON plugins (show_order);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS tasks;

View File

@@ -0,0 +1,12 @@
CREATE TABLE tasks
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(255) NOT NULL,
status varchar(255) DEFAULT 'waiting' NOT NULL,
shell varchar(255) DEFAULT NULL,
log varchar(255) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE INDEX tasks_status_index ON tasks (status);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS monitors;

View File

@@ -0,0 +1,7 @@
CREATE TABLE monitors
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
info text NOT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS crons;

View File

@@ -0,0 +1,16 @@
CREATE TABLE crons
(
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(255) NOT NULL,
status boolean DEFAULT 0 NOT NULL,
type varchar(255) NOT NULL,
time varchar(255) NOT NULL,
shell varchar(255) DEFAULT NULL,
log varchar(255) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
);
CREATE UNIQUE INDEX crons_name_unique ON crons (name);
CREATE INDEX crons_status_index ON crons (status);
CREATE INDEX crons_type_index ON crons (type);

171
go.mod Normal file
View File

@@ -0,0 +1,171 @@
module panel
go 1.18
require (
github.com/goravel/framework v1.12.2
google.golang.org/grpc v1.56.0
)
require (
cloud.google.com/go v0.110.0 // indirect
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/pubsub v1.30.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae // indirect
github.com/RichardKnop/machinery/v2 v2.0.11 // indirect
github.com/aws/aws-sdk-go v1.37.16 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v20.10.22+incompatible // indirect
github.com/docker/docker v20.10.24+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-contrib/static v0.0.1 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/glebarez/go-sqlite v1.21.1 // indirect
github.com/glebarez/sqlite v1.8.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-redsync/redsync/v4 v4.0.4 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-migrate/migrate/v4 v4.16.2 // indirect
github.com/golang-module/carbon/v2 v2.2.3 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.3 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
github.com/gookit/color v1.5.3 // indirect
github.com/gookit/filter v1.1.4 // indirect
github.com/gookit/goutil v0.5.15 // indirect
github.com/gookit/validate v1.4.6 // indirect
github.com/goravel/file-rotatelogs v0.0.0-20211215053220-2ab31dd9575c // indirect
github.com/goravel/file-rotatelogs/v2 v2.4.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lestrrat-go/strftime v1.0.5 // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/microsoft/go-mssqldb v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/ory/dockertest/v3 v3.10.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rs/cors v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/streadway/amqp v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/unrolled/secure v1.13.0 // indirect
github.com/urfave/cli/v2 v2.25.6 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.7.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/api v0.122.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230525234025-438c736192d0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.1 // indirect
gorm.io/driver/postgres v1.5.2 // indirect
gorm.io/driver/sqlserver v1.5.1 // indirect
gorm.io/gorm v1.25.1 // indirect
gorm.io/plugin/dbresolver v1.4.1 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.21.1 // indirect
)

1093
go.sum Normal file

File diff suppressed because it is too large Load Diff

39
main.go Normal file
View File

@@ -0,0 +1,39 @@
/*
Copyright [2022] [HaoZi Technology Co., Ltd.]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"github.com/goravel/framework/facades"
"panel/bootstrap"
)
func main() {
// 启动框架
bootstrap.Boot()
// 加载插件
bootstrap.Plugins()
// 启动 HTTP 服务
go func() {
if err := facades.Route().Run(); err != nil {
facades.Log().Errorf("Route run error: %v", err)
}
}()
select {}
}

285
packages/helpers/helpers.go Normal file
View File

@@ -0,0 +1,285 @@
// Package helpers 存放辅助方法
package helpers
import (
"bufio"
"bytes"
"crypto/md5"
"crypto/rand"
"fmt"
"io"
"os"
"os/exec"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
)
// Empty 类似于 PHP 的 empty() 函数
func Empty(val interface{}) bool {
if val == nil {
return true
}
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.String, reflect.Array:
return v.Len() == 0
case reflect.Map, reflect.Slice:
return v.Len() == 0 || v.IsNil()
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
}
// FirstElement 安全地获取 args[0],避免 panic: runtime error: index out of range
func FirstElement(args []string) string {
if len(args) > 0 {
return args[0]
}
return ""
}
// RandomNumber 生成长度为 length 随机数字字符串
func RandomNumber(length int) string {
table := [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
b := make([]byte, length)
n, err := io.ReadAtLeast(rand.Reader, b, length)
if n != length {
panic(err)
}
for i := 0; i < len(b); i++ {
b[i] = table[int(b[i])%len(table)]
}
return string(b)
}
// RandomString 生成长度为 length 的随机字符串
func RandomString(length int) string {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
letters := "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
for i, v := range b {
b[i] = letters[v%byte(len(letters))]
}
return string(b)
}
// MD5 生成字符串的 MD5 值
func MD5(str string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(str)))
}
// FormatBytes 格式化bytes
func FormatBytes(size float64) string {
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
for ; size >= 1024 && i < 4; i++ {
size /= 1024
}
return fmt.Sprintf("%.2f %s", size, units[i])
}
// Cut 裁剪字符串
func Cut(begin, end, str string) string {
b := utf8.RuneCountInString(str[:strings.Index(str, begin)]) + utf8.RuneCountInString(begin)
e := utf8.RuneCountInString(str[:strings.Index(str, end)]) - b
return string([]rune(str)[b : b+e])
}
// GetNetInfo 获取网络统计信息
func GetNetInfo() (uint64, uint64) {
file, err := os.Open("/proc/net/dev")
if err != nil {
panic(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
allRs := make(map[string][]string)
lineNumber := 0
for scanner.Scan() {
lineNumber++
if lineNumber < 3 {
continue
}
line := strings.TrimSpace(scanner.Text())
line = strings.Replace(line, ":", " ", -1)
re := regexp.MustCompile("[ ]+")
line = re.ReplaceAllString(line, " ")
arr := strings.Split(line, " ")
if len(arr) > 0 && arr[0] != "" {
allRs[arr[0]+strconv.Itoa(lineNumber)] = []string{arr[0], arr[1], arr[9]}
}
}
var keys []string
for key := range allRs {
keys = append(keys, key)
}
sort.Strings(keys)
tx := uint64(0)
rx := uint64(0)
for _, key := range keys {
if strings.Contains(key, "lo") {
continue
}
val := allRs[key]
txValue, err := strconv.ParseUint(val[2], 10, 64)
if err == nil {
tx += txValue
}
rxValue, err := strconv.ParseUint(val[1], 10, 64)
if err == nil {
rx += rxValue
}
}
return tx, rx
}
// MonitoringInfo 监控信息
type MonitoringInfo struct {
CpuUse float64 `json:"cpu_use"`
Uptime float64 `json:"uptime"`
UptimePercent float64 `json:"uptime_percent"`
MemTotal float64 `json:"mem_total"`
MemUse float64 `json:"mem_use"`
MemUsePercent float64 `json:"mem_use_percent"`
SwapTotal float64 `json:"swap_total"`
SwapUse float64 `json:"swap_use"`
SwapUsePercent float64 `json:"swap_use_percent"`
NetTx uint64 `json:"net_tx"`
NetRx uint64 `json:"net_rx"`
}
// GetMonitoringInfo 获取监控数据
func GetMonitoringInfo() (MonitoringInfo, error) {
var res MonitoringInfo
// 网络流量
netTx1, netRx1 := GetNetInfo()
time.Sleep(time.Second)
netTx2, netRx2 := GetNetInfo()
res.NetTx = netTx2 - netTx1
res.NetRx = netRx2 - netRx1
// CPU 信息
cpuInfoRaw, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return MonitoringInfo{}, err
}
physicalArr := make(map[string]struct{})
var siblingsSum float64
var re = regexp.MustCompile(`\d+\.\d+`)
uptimeOutput, err := exec.Command("uptime").Output()
if err != nil {
return MonitoringInfo{}, err
}
uptimeValues := re.FindAllString(string(uptimeOutput), -1)
uptime1, _ := strconv.ParseFloat(uptimeValues[0], 64)
res.Uptime = uptime1
processors := bytes.Split(cpuInfoRaw, []byte("\nprocessor"))
rePhysical := regexp.MustCompile(`physical id\s*:\s(.*)`)
reSiblings := regexp.MustCompile(`siblings\s*:\s(.*)`)
for _, v := range processors {
physical := rePhysical.FindSubmatch(v)
siblings := reSiblings.FindSubmatch(v)
if len(physical) > 1 {
pid := string(physical[1])
if _, found := physicalArr[pid]; !found {
if len(siblings) > 1 {
siblingsValue, _ := strconv.ParseFloat(string(siblings[1]), 64)
siblingsSum += siblingsValue
}
physicalArr[pid] = struct{}{}
}
}
}
// CPU 使用率
cpuUse := 0.1
psOutput, err := exec.Command("ps", "aux").Output()
if err != nil {
return MonitoringInfo{}, err
}
cpuRaw := strings.Split(string(psOutput), "\n")
pid := os.Getpid()
for _, v := range cpuRaw {
v = strings.TrimSpace(v)
v = regexp.MustCompile(`\s+`).ReplaceAllString(v, " ")
values := strings.Split(v, " ")
if len(values) > 2 {
p, _ := strconv.Atoi(values[1])
if p == pid {
continue
}
cpu, _ := strconv.ParseFloat(values[2], 64)
cpuUse += cpu
}
}
cpuUse = cpuUse / siblingsSum
if cpuUse > 100 {
cpuUse = 100
}
res.CpuUse = cpuUse
// 内存使用率
freeOutput, err := exec.Command("free", "-m").Output()
if err != nil {
return MonitoringInfo{}, err
}
memRaw := strings.Split(string(freeOutput), "\n")
var memList, swapList string
for _, v := range memRaw {
if strings.Contains(v, "Mem") {
memList = regexp.MustCompile(`\s+`).ReplaceAllString(v, " ")
} else if strings.Contains(v, "Swap") {
swapList = regexp.MustCompile(`\s+`).ReplaceAllString(v, " ")
}
}
memArr := strings.Split(memList, " ")
swapArr := strings.Split(swapList, " ")
memTotal, _ := strconv.ParseFloat(memArr[1], 64)
swapTotal, _ := strconv.ParseFloat(swapArr[1], 64)
memUse, _ := strconv.ParseFloat(memArr[2], 64)
swapUse, _ := strconv.ParseFloat(swapArr[2], 64)
memUseP := (memUse / memTotal) * 100
swapUseP := (swapUse / swapTotal) * 100
uptime1P := uptime1 * 10
if uptime1P > 100 {
uptime1P = 100
}
res.MemTotal = memTotal
res.MemUse = memUse
res.MemUsePercent = memUseP
res.SwapTotal = swapTotal
res.SwapUse = swapUse
res.SwapUsePercent = swapUseP
res.UptimePercent = uptime1P
return res, nil
}

View File

@@ -0,0 +1,21 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
)
type OpenRestyController struct {
//Dependent services
}
func NewOpenrestyController() *OpenRestyController {
return &OpenRestyController{
//Inject services
}
}
func (r *OpenRestyController) Show(ctx http.Context) {
ctx.Response().Success().Json(http.Json{
"Hello": "Goravel",
})
}

View File

@@ -0,0 +1,13 @@
package openresty
const (
Name = "OpenResty"
Author = "耗子"
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
Slug = "openresty"
Version = "1.21.4.1"
)
func Boot() {
Route()
}

View File

@@ -0,0 +1,22 @@
package openresty
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/route"
"github.com/goravel/framework/facades"
"panel/plugins/openresty/http/controllers"
)
func Route() {
facades.Route().Prefix("api/plugins/openresty").Group(func(route route.Route) {
route.Get("/openresty", func(ctx http.Context) {
ctx.Response().Json(http.StatusOK, http.Json{
"Hello": "Openresty",
})
})
openRestyController := controllers.NewOpenrestyController()
route.Get("/openresty/users/{id}", openRestyController.Show)
})
}

30
public/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
pnpm-lock.yaml

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

52
public/index.html Normal file
View File

@@ -0,0 +1,52 @@
<!--
////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永无Bug 永不宕机 //
// Name耗子Linux面板 Author耗子 Date2023-06-21 //
////////////////////////////////////////////////////////////////////
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>加载中...</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="./res/layui/css/layui.css" rel="stylesheet">
<script src="https://cdnjs.cdn.haozi.net/ace/1.6.1/ace.js"></script>
</head>
<body>
<div id="Panel_app"></div>
<script src="./res/layui/layui.js"></script>
<script>
layui.config({
base: 'res/', // 静态资源所在路径
version: new Date().getTime()
}).use('index', function () {
var layer = layui.layer
var admin = layui.admin
})
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,182 @@
/**
* admin.login.css
*/
html, body, #Panel_app {
height: 100%;
}
.layui-layout-body {
overflow: auto;
}
#LAY-user-login,
.layadmin-user-display-show {
display: block !important;
}
.layadmin-user-login {
position: relative;
left: 0;
top: 0;
padding: 110px 0;
min-height: 100%;
box-sizing: border-box;
}
.layadmin-user-login-main {
width: 375px;
margin: 0 auto;
box-sizing: border-box;
}
.layadmin-user-login-box {
padding: 20px;
}
.layadmin-user-login-header {
text-align: center;
}
.layadmin-user-login-header h2 {
margin-bottom: 10px;
font-weight: 300;
font-size: 30px;
color: #000;
}
.layadmin-user-login-header p {
font-weight: 300;
color: #999;
}
.layadmin-user-login-body .layui-form-item {
position: relative;
}
.layadmin-user-login-icon {
position: absolute;
left: 1px;
top: 1px;
width: 38px;
line-height: 36px;
text-align: center;
color: #d2d2d2;
}
.layadmin-user-login-body .layui-form-item .layui-input {
padding-left: 38px;
}
.layadmin-user-login-codeimg {
max-height: 38px;
width: 100%;
cursor: pointer;
box-sizing: border-box;
}
.layadmin-user-login-other {
position: relative;
font-size: 0;
line-height: 38px;
padding-top: 20px;
}
.layadmin-user-login-other > * {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
}
.layadmin-user-login-other .layui-icon {
position: relative;
top: 2px;
font-size: 26px;
}
.layadmin-user-login-other a:hover {
opacity: 0.8;
}
.layadmin-user-jump-change {
float: right;
}
.layadmin-user-login-footer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
line-height: 30px;
padding: 20px;
text-align: center;
box-sizing: border-box;
color: rgba(0, 0, 0, .5)
}
.layadmin-user-login-footer span {
padding: 0 5px;
}
.layadmin-user-login-footer a {
padding: 0 5px;
color: rgba(0, 0, 0, .5);
}
.layadmin-user-login-footer a:hover {
color: rgba(0, 0, 0, 1);
}
/* 有背景图时 */
.layadmin-user-login-main[bgimg] {
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
}
/* 主题背景 */
.ladmin-user-login-theme {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
}
.ladmin-user-login-theme ul {
display: inline-block;
padding: 5px;
background-color: #fff;
}
.ladmin-user-login-theme ul li {
display: inline-block;
vertical-align: top;
width: 64px;
height: 43px;
cursor: pointer;
transition: all .3s;
-webkit-transition: all .3s;
background-color: #f2f2f2;
}
.ladmin-user-login-theme ul li:hover {
opacity: 0.9
}
@media screen and (max-width: 768px) {
.layadmin-user-login {
padding-top: 60px;
}
.layadmin-user-login-main {
width: 300px;
}
.layadmin-user-login-box {
padding: 10px;
}
}

View File

@@ -0,0 +1,869 @@
/**
* 界面核心模块
*/
layui.define('view', function (exports) {
var $ = layui.jquery
, laytpl = layui.laytpl
, element = layui.element
, table = layui.table
, upload = layui.upload
, setter = layui.setter
, view = layui.view
, device = layui.device()
, $win = $(window), $body = $('body')
, container = $('#' + setter.container)
, SHOW = 'layui-show', HIDE = 'layui-hide', THIS = 'layui-this', DISABLED = 'layui-disabled', TEMP = 'template'
, APP_BODY = '#Panel_app_body', APP_FLEXIBLE = 'Panel_app_flexible'
, FILTER_TAB_TBAS = 'layadmin-layout-tabs'
, APP_SPREAD_SM = 'layadmin-side-spread-sm', TABS_BODY = 'layadmin-tabsbody-item'
, ICON_SHRINK = 'layui-icon-shrink-right', ICON_SPREAD = 'layui-icon-spread-left'
, SIDE_SHRINK = 'layadmin-side-shrink', SIDE_MENU = 'LAY-system-side-menu'
//通用方法
, admin = {
v: '2.0.0'
, mode: 'spa'
//数据的异步请求
, req: view.req
//清除本地 token并跳转到登入页
, exit: view.exit
//xss 转义
, escape: function (html) {
return String(html || '').replace(/&(?!#?[a-zA-Z0-9]+;)/g, '&amp;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/'/g, '&#39;').replace(/"/g, '&quot;')
}
//事件
, on: function (events, callback) {
return layui.onevent.call(this, setter.MOD_NAME, events, callback)
}
//弹出面板
, popup: view.popup
//右侧面板
, popupRight: function (options) {
//layer.close(admin.popup.index);
return admin.popup.index = layer.open($.extend({
type: 1
, id: 'LAY_adminPopupR'
, anim: -1
, title: false
, closeBtn: false
, offset: 'r'
, shade: 0.1
, shadeClose: true
, skin: 'layui-anim layui-anim-rl layui-layer-adminRight'
, area: '300px'
}, options))
}
//发送验证码
, sendAuthCode: function (options) {
options = $.extend({
seconds: 60
, elemPhone: '#LAY_phone'
, elemVercode: '#LAY_vercode'
}, options)
var seconds = options.seconds
, token = null
, timer, countDown = function (loop) {
var btn = $(options.elem)
seconds--
if (seconds < 0) {
btn.removeClass(DISABLED).html('获取验证码')
seconds = options.seconds
clearInterval(timer)
} else {
btn.addClass(DISABLED).html(seconds + '秒后重获')
}
if (!loop) {
timer = setInterval(function () {
countDown(true)
}, 1000)
}
}
$body.off('click', options.elem).on('click', options.elem, function () {
options.elemPhone = $(options.elemPhone)
options.elemVercode = $(options.elemVercode)
var elemPhone = options.elemPhone
, value = elemPhone.val()
if (seconds !== options.seconds || $(this).hasClass(DISABLED)) return
if (!/^1\d{10}$/.test(value)) {
elemPhone.focus()
return layer.msg('请输入正确的手机号')
}
if (typeof options.ajax === 'object') {
var success = options.ajax.success
delete options.ajax.success
}
admin.req($.extend(true, {
url: '/auth/code'
, type: 'get'
, data: {
phone: value
}
, success: function (res) {
layer.msg('验证码已发送至你的手机,请注意查收', {
icon: 1
, shade: 0
})
options.elemVercode.focus()
countDown()
success && success(res)
}
}, options.ajax))
})
}
//屏幕类型
, screen: function () {
var width = $win.width()
if (width > 1200) {
return 3 //大屏幕
} else if (width > 992) {
return 2 //中屏幕
} else if (width > 768) {
return 1 //小屏幕
} else {
return 0 //超小屏幕
}
}
//侧边伸缩
, sideFlexible: function (status) {
var app = container
, iconElem = $('#' + APP_FLEXIBLE)
, screen = admin.screen()
//设置状态PC默认展开、移动默认收缩
if (status === 'spread') {
//切换到展开状态的 icon箭头
iconElem.removeClass(ICON_SPREAD).addClass(ICON_SHRINK)
//移动从左到右位移PC清除多余选择器恢复默认
if (screen < 2) {
app.addClass(APP_SPREAD_SM)
} else {
app.removeClass(APP_SPREAD_SM)
}
app.removeClass(SIDE_SHRINK)
} else {
//切换到搜索状态的 icon箭头
iconElem.removeClass(ICON_SHRINK).addClass(ICON_SPREAD)
//移动清除多余选择器恢复默认PC从右往左收缩
if (screen < 2) {
app.removeClass(SIDE_SHRINK)
} else {
app.addClass(SIDE_SHRINK)
}
app.removeClass(APP_SPREAD_SM)
}
layui.event.call(this, setter.MOD_NAME, 'side({*})', {
status: status
})
}
//重置主体区域表格尺寸
, resizeTable: function (delay) {
var that = this, runResizeTable = function () {
that.tabsBody(admin.tabsPage.index).find('.layui-table-view').each(function () {
var tableID = $(this).attr('lay-id')
layui.table.resize(tableID)
})
}
if (!layui.table) return
delay ? setTimeout(runResizeTable, delay) : runResizeTable()
}
//主题设置
, theme: function (options) {
var theme = setter.theme
, local = layui.data(setter.tableName)
, id = 'LAY_layadmin_theme'
, style = document.createElement('style')
, styleText = laytpl([
//主题色
'.layui-side-menu,'
, '.layui-layer-admin .layui-layer-title,'
, '.layadmin-side-shrink .layui-side-menu .layui-nav>.layui-nav-item>.layui-nav-child'
, '{background-color:{{d.color.main}} !important;}'
//背景选中色
, '.layadmin-pagetabs .layui-tab-title li:after,'
, '.layadmin-pagetabs .layui-tab-title li.layui-this:after,'
, '.layui-nav-tree .layui-this,'
, '.layui-nav-tree .layui-this>a,'
, '.layui-nav-tree .layui-nav-child dd.layui-this,'
, '.layui-nav-tree .layui-nav-child dd.layui-this a,'
, '.layui-nav-tree .layui-nav-bar'
, '{background-color:{{d.color.selected}} !important;}'
//logo
, '.layui-layout-admin .layui-logo{background-color:{{d.color.logo || d.color.main}} !important;}'
//文字选中色
, '.layadmin-pagetabs .layui-tab-title li:hover,'
, '.layadmin-pagetabs .layui-tab-title li.layui-this'
, '{color: {{d.color.selected}} !important;}'
//头部色
, '{{# if(d.color.header){ }}'
, '.layui-layout-admin .layui-header{background-color:{{ d.color.header }};}'
, '.layui-layout-admin .layui-header a,'
, '.layui-layout-admin .layui-header a cite{color: #f8f8f8;}'
, '.layui-layout-admin .layui-header a:hover{color: #fff;}'
, '.layui-layout-admin .layui-header .layui-nav .layui-nav-more{border-top-color: #fbfbfb;}'
, '.layui-layout-admin .layui-header .layui-nav .layui-nav-mored{border-color: transparent; border-bottom-color: #fbfbfb;}'
, '.layui-layout-admin .layui-header .layui-nav .layui-this:after, .layui-layout-admin .layui-header .layui-nav-bar{background-color: #fff; background-color: rgba(255,255,255,.5);}'
, '.layadmin-pagetabs .layui-tab-title li:after{display: none;}'
, '{{# } }}'
].join('')).render(options = $.extend({}, local.theme, options))
, styleElem = document.getElementById(id)
//添加主题样式
if ('styleSheet' in style) {
style.setAttribute('type', 'text/css')
style.styleSheet.cssText = styleText
} else {
style.innerHTML = styleText
}
style.id = id
styleElem && $body[0].removeChild(styleElem)
$body[0].appendChild(style)
$body.attr('layadmin-themealias', options.color.alias)
//本地存储记录
local.theme = local.theme || {}
layui.each(options, function (key, value) {
local.theme[key] = value
})
layui.data(setter.tableName, {
key: 'theme'
, value: local.theme
})
}
//初始化主题
, initTheme: function (index) {
var theme = setter.theme
index = index || 0
if (theme.color[index]) {
theme.color[index].index = index
admin.theme({
color: theme.color[index]
})
}
}
//记录最近一次点击的页面标签数据
, tabsPage: {}
//获取标签页的头元素
, tabsHeader: function (index) {
return $('#Panel_app_tabsheader').children('li').eq(index || 0)
}
//获取页面标签主体元素
, tabsBody: function (index) {
return $(APP_BODY).find('.' + TABS_BODY).eq(index || 0)
}
//切换页面标签主体
, tabsBodyChange: function (index) {
admin.tabsHeader(index).attr('lay-attr', layui.router().href)
admin.tabsBody(index).addClass(SHOW).siblings().removeClass(SHOW)
events.rollPage('auto', index)
}
//resize事件管理
, resize: function (fn) {
var router = layui.router()
, key = router.path.join('-')
if (admin.resizeFn[key]) {
$win.off('resize', admin.resizeFn[key])
delete admin.resizeFn[key]
}
if (fn === 'off') return //如果是清除 resize 事件,则终止往下执行
fn(), admin.resizeFn[key] = fn
$win.on('resize', admin.resizeFn[key])
}
, resizeFn: {}
, runResize: function () {
var router = layui.router()
, key = router.path.join('-')
admin.resizeFn[key] && admin.resizeFn[key]()
}
, delResize: function () {
this.resize('off')
}
//关闭当前 pageTabs
, closeThisTabs: function () {
if (!admin.tabsPage.index) return
$(TABS_HEADER).eq(admin.tabsPage.index).find('.layui-tab-close').trigger('click')
}
//全屏
, fullScreen: function () {
var ele = document.documentElement
, reqFullScreen = ele.requestFullScreen || ele.webkitRequestFullScreen
|| ele.mozRequestFullScreen || ele.msRequestFullscreen
if (typeof reqFullScreen !== 'undefined' && reqFullScreen) {
reqFullScreen.call(ele)
}
}
//退出全屏
, exitScreen: function () {
var ele = document.documentElement
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
}
//纠正单页路由格式
, correctRouter: function (href) {
if (!/^\//.test(href)) href = '/' + href
//纠正首尾
return href.replace(/^(\/+)/, '/')
.replace(new RegExp('\/' + setter.entry + '$'), '/') //过滤路由最后的默认视图文件名index
}
//……
}
//事件
var events = admin.events = {
//伸缩
flexible: function (othis) {
var iconElem = othis.find('#' + APP_FLEXIBLE)
, isSpread = iconElem.hasClass(ICON_SPREAD)
admin.sideFlexible(isSpread ? 'spread' : null) //控制伸缩
admin.resizeTable(350)
}
//刷新
, refresh: function () {
admin.render()
}
//输入框搜索
, serach: function (othis) {
othis.off('keypress').on('keypress', function (e) {
if (!this.value.replace(/\s/g, '')) return
//回车跳转
if (e.keyCode === 13) {
var href = othis.attr('lay-action')
, text = othis.attr('lay-text') || '搜索'
href = href + this.value
text = text + ' <span style="color: #FF5722;">' + admin.escape(this.value) + '</span>'
//打开标签页
location.hash = admin.correctRouter(href)
//如果搜索关键词已经打开,则刷新页面即可
events.serach.keys || (events.serach.keys = {})
events.serach.keys[admin.tabsPage.index] = this.value
if (this.value === events.serach.keys[admin.tabsPage.index]) {
events.refresh(othis)
}
//清空输入框
this.value = ''
}
})
}
//点击消息
, message: function (othis) {
othis.find('.layui-badge-dot').remove()
}
//弹出主题面板
, theme: function () {
admin.popupRight({
id: 'LAY_adminPopupTheme'
, success: function () {
view(this.id).render('system/theme')
}
})
}
//便签
, note: function (othis) {
var mobile = admin.screen() < 2
, note = layui.data(setter.tableName).note
events.note.index = admin.popup({
title: '便签'
, shade: 0
, offset: [
'41px'
, (mobile ? null : (othis.offset().left - 250) + 'px')
]
, anim: -1
, id: 'LAY_adminNote'
, skin: 'layadmin-note layui-anim layui-anim-upbit'
, content: '<textarea placeholder="内容"></textarea>'
, resize: false
, success: function (layero, index) {
var textarea = layero.find('textarea')
,
value = note === undefined ? '便签中的内容会存储在本地,这样即便你关掉了浏览器,在下次打开时,依然会读取到上一次的记录。是个非常小巧实用的本地备忘录' : note
textarea.val(value).focus().on('keyup', function () {
layui.data(setter.tableName, {
key: 'note'
, value: this.value
})
})
}
})
}
//全屏
, fullscreen: function (othis) {
var SCREEN_FULL = 'layui-icon-screen-full'
, SCREEN_REST = 'layui-icon-screen-restore'
, iconElem = othis.children('i')
if (iconElem.hasClass(SCREEN_FULL)) {
admin.fullScreen()
iconElem.addClass(SCREEN_REST).removeClass(SCREEN_FULL)
} else {
admin.exitScreen()
iconElem.addClass(SCREEN_FULL).removeClass(SCREEN_REST)
}
}
//弹出关于面板
, about: function () {
admin.popupRight({
id: 'LAY_adminPopupAbout'
, success: function () {
view(this.id).render('system/about')
}
})
}
//弹出更多面板
, more: function () {
admin.popupRight({
id: 'LAY_adminPopupMore'
, success: function () {
view(this.id).render('system/more')
}
})
}
//返回上一页
, back: function () {
history.back()
}
//主题设置
, setTheme: function (othis) {
var index = othis.data('index')
, nextIndex = othis.siblings('.layui-this').data('index')
if (othis.hasClass(THIS)) return
othis.addClass(THIS).siblings('.layui-this').removeClass(THIS)
admin.initTheme(index)
}
//左右滚动页面标签
, rollPage: function (type, index) {
var tabsHeader = $('#Panel_app_tabsheader')
, liItem = tabsHeader.children('li')
, scrollWidth = tabsHeader.prop('scrollWidth')
, outerWidth = tabsHeader.outerWidth()
, tabsLeft = parseFloat(tabsHeader.css('left'))
//右左往右
if (type === 'left') {
if (!tabsLeft && tabsLeft <= 0) return
//当前的left减去可视宽度用于与上一轮的页标比较
var prefLeft = -tabsLeft - outerWidth
liItem.each(function (index, item) {
var li = $(item)
, left = li.position().left
if (left >= prefLeft) {
tabsHeader.css('left', -left)
return false
}
})
} else if (type === 'auto') { //自动滚动
(function () {
var thisLi = liItem.eq(index), thisLeft
if (!thisLi[0]) return
thisLeft = thisLi.position().left
//当目标标签在可视区域左侧时
if (thisLeft < -tabsLeft) {
return tabsHeader.css('left', -thisLeft)
}
//当目标标签在可视区域右侧时
if (thisLeft + thisLi.outerWidth() >= outerWidth - tabsLeft) {
var subLeft = thisLeft + thisLi.outerWidth() - (outerWidth - tabsLeft)
liItem.each(function (i, item) {
var li = $(item)
, left = li.position().left
//从当前可视区域的最左第二个节点遍历,如果减去最左节点的差 > 目标在右侧不可见的宽度,则将该节点放置可视区域最左
if (left + tabsLeft > 0) {
if (left - tabsLeft > subLeft) {
tabsHeader.css('left', -left)
return false
}
}
})
}
}())
} else {
//默认向左滚动
liItem.each(function (i, item) {
var li = $(item)
, left = li.position().left
if (left + li.outerWidth() >= outerWidth - tabsLeft) {
tabsHeader.css('left', -left)
return false
}
})
}
}
//向右滚动页面标签
, leftPage: function () {
events.rollPage('left')
}
//向左滚动页面标签
, rightPage: function () {
events.rollPage()
}
//关闭当前标签页
, closeThisTabs: function () {
admin.closeThisTabs()
}
//关闭其它标签页
, closeOtherTabs: function (type) {
var TABS_REMOVE = 'LAY-system-pagetabs-remove'
if (type === 'all') {
$(TABS_HEADER + ':gt(0)').remove()
$(APP_BODY).find('.' + TABS_BODY + ':gt(0)').remove()
} else {
$(TABS_HEADER).each(function (index, item) {
if (index && index != admin.tabsPage.index) {
$(item).addClass(TABS_REMOVE)
admin.tabsBody(index).addClass(TABS_REMOVE)
}
})
$('.' + TABS_REMOVE).remove()
}
}
//关闭全部标签页
, closeAllTabs: function () {
events.closeOtherTabs('all')
location.hash = ''
}
//遮罩
, shade: function () {
admin.sideFlexible()
}
}
//初始
!function () {
//主题初始化,本地主题记录优先,其次为 initColorIndex
var local = layui.data(setter.tableName)
if (local.theme) {
admin.theme(local.theme)
} else if (setter.theme) {
admin.initTheme(setter.theme.initColorIndex)
}
//禁止水平滚动
$body.addClass('layui-layout-body')
//移动端强制不开启页面标签功能
if (admin.screen() < 1) {
delete setter.pageTabs
}
//不开启页面标签时
if (!setter.pageTabs) {
container.addClass('layadmin-tabspage-none')
}
//低版本IE提示
if (device.ie && device.ie < 10) {
view.error('IE' + device.ie + '下访问可能不佳推荐使用Chrome / Firefox / Edge 等高级浏览器', {
offset: 'auto'
, id: 'LAY_errorIE'
})
}
}()
//admin.prevRouter = {}; //上一个路由
// hash 改变侧边状态
admin.on('hash(side)', function (router) {
var path = router.path, getData = function (item) {
return {
list: item.children('.layui-nav-child')
, name: item.data('name')
, jump: item.data('jump')
}
}
, sideMenu = $('#' + SIDE_MENU)
, SIDE_NAV_ITEMD = 'layui-nav-itemed'
//捕获对应菜单
, matchMenu = function (list) {
var pathURL = admin.correctRouter(path.join('/'))
list.each(function (index1, item1) {
var othis1 = $(item1)
, data1 = getData(othis1)
, listChildren1 = data1.list.children('dd')
, matched1 = path[0] == data1.name || (index1 === 0 && !path[0])
|| (data1.jump && pathURL == admin.correctRouter(data1.jump))
listChildren1.each(function (index2, item2) {
var othis2 = $(item2)
, data2 = getData(othis2)
, listChildren2 = data2.list.children('dd')
, matched2 = (path[0] == data1.name && path[1] == data2.name)
|| (data2.jump && pathURL == admin.correctRouter(data2.jump))
listChildren2.each(function (index3, item3) {
var othis3 = $(item3)
, data3 = getData(othis3)
, matched3 = (path[0] == data1.name && path[1] == data2.name && path[2] == data3.name)
|| (data3.jump && pathURL == admin.correctRouter(data3.jump))
if (matched3) {
var selected = data3.list[0] ? SIDE_NAV_ITEMD : THIS
othis3.addClass(selected).siblings().removeClass(selected) //标记选择器
return false
}
})
if (matched2) {
var selected = data2.list[0] ? SIDE_NAV_ITEMD : THIS
othis2.addClass(selected).siblings().removeClass(selected) //标记选择器
return false
}
})
if (matched1) {
var selected = data1.list[0] ? SIDE_NAV_ITEMD : THIS
othis1.addClass(selected).siblings().removeClass(selected) //标记选择器
return false
}
})
}
//重置状态
sideMenu.find('.' + THIS).removeClass(THIS)
//移动端点击菜单时自动收缩
if (admin.screen() < 2) admin.sideFlexible()
//开始捕获
matchMenu(sideMenu.children('li'))
})
//侧边导航点击事件
element.on('nav(layadmin-system-side-menu)', function (elem) {
if (elem.siblings('.layui-nav-child')[0] && container.hasClass(SIDE_SHRINK)) {
admin.sideFlexible('spread')
layer.close(elem.data('index'))
}
admin.tabsPage.type = 'nav'
})
//选项卡的更多操作
element.on('nav(layadmin-pagetabs-nav)', function (elem) {
var dd = elem.parent()
dd.removeClass(THIS)
dd.parent().removeClass(SHOW)
})
//同步路由
var setThisRouter = function (othis) {
var layid = othis.attr('lay-id')
, attr = othis.attr('lay-attr')
, index = othis.index()
location.hash = layid === setter.entry ? '/' : (attr || '/')
admin.tabsBodyChange(index)
}
, TABS_HEADER = '#Panel_app_tabsheader>li'
//页面标签点击
$body.on('click', TABS_HEADER, function () {
var othis = $(this)
, index = othis.index()
admin.tabsPage.type = 'tab'
admin.tabsPage.index = index
//如果是iframe类型的标签页
if (othis.attr('lay-attr') === 'iframe') {
return admin.tabsBodyChange(index)
}
setThisRouter(othis) //同步路由
admin.runResize() //执行resize事件如果存在的话
admin.resizeTable() //重置当前主体区域的表格尺寸
})
// tabspage 删除
element.on('tabDelete(layadmin-layout-tabs)', function (obj) {
var othis = $(TABS_HEADER + '.layui-this')
obj.index && admin.tabsBody(obj.index).remove()
setThisRouter(othis)
//移除resize事件
admin.delResize()
})
// 页面跳转
$body.on('click', '*[lay-href]', function () {
var othis = $(this)
var href = othis.attr('lay-href')
var router = layui.router()
admin.tabsPage.elem = othis
// admin.prevRouter[router.path[0]] = router.href; //记录上一次各菜单的路由信息
// 执行跳转
location.hash = admin.correctRouter(href)
// 如果为当前页,则执行刷新
if (setter.refreshCurrPage) {
if (admin.correctRouter(href) === router.href) {
admin.events.refresh()
}
}
})
//点击事件
$body.on('click', '*[layadmin-event]', function () {
var othis = $(this)
, attrEvent = othis.attr('layadmin-event')
events[attrEvent] && events[attrEvent].call(this, othis)
})
//tips
$body.on('mouseenter', '*[lay-tips]', function () {
var othis = $(this)
if (othis.parent().hasClass('layui-nav-item') && !container.hasClass(SIDE_SHRINK)) return
var tips = othis.attr('lay-tips')
, offset = othis.attr('lay-offset')
, direction = othis.attr('lay-direction')
, index = layer.tips(tips, this, {
tips: direction || 1
, time: -1
, success: function (layero, index) {
if (offset) {
layero.css('margin-left', offset + 'px')
}
}
})
othis.data('index', index)
}).on('mouseleave', '*[lay-tips]', function () {
layer.close($(this).data('index'))
})
//窗口resize事件
var resizeSystem = layui.data.resizeSystem = function () {
//layer.close(events.note.index);
layer.closeAll('tips')
if (!resizeSystem.lock) {
setTimeout(function () {
admin.sideFlexible(admin.screen() < 2 ? '' : 'spread')
delete resizeSystem.lock
}, 100)
}
resizeSystem.lock = true
}
$win.on('resize', layui.data.resizeSystem)
//设置组件全局 token
!function () {
var request = setter.request
if (request.tokenName) {
var obj = {}
obj[request.tokenName] = layui.data(setter.tableName)[request.tokenName] || ''
//table
table.set({
headers: obj, //通过 request 头传递
where: obj //通过参数传递
})
//upload
upload.set({
headers: obj, //通过 request 头传递
data: obj //通过参数传递
})
}
}()
//接口输出
exports('admin', admin)
})

View File

@@ -0,0 +1,198 @@
/**
* 界面入口模块
*/
layui.define('admin', function (exports) {
var setter = layui.setter
var element = layui.element
var admin = layui.admin
var tabsPage = admin.tabsPage
var view = layui.view
//根据路由渲染页面
var renderPage = function () {
var router = layui.router()
, path = router.path
, pathURL = admin.correctRouter(router.path.join('/'))
//默认读取主页
if (!path.length) path = ['']
//如果最后一项为空字符,则读取默认文件
if (path[path.length - 1] === '') {
path[path.length - 1] = setter.entry
}
//重置状态
var reset = function (type) {
//renderPage.haveInit && layer.closeAll();
if (renderPage.haveInit) {
$('.layui-layer').each(function () {
var othis = $(this),
index = othis.attr('times')
if (!(othis.hasClass('layui-layim') || othis.hasClass('layui-layim-chat'))) {
layer.close(index)
}
})
}
renderPage.haveInit = true
$(APP_BODY).scrollTop(0)
delete tabsPage.type //重置页面标签的来源类型
}
//如果路由来自于 tab 切换,则不重新请求视图
if (tabsPage.type === 'tab') {
//切换到非主页、或者切换到主页且主页必须有内容。方可阻止请求
if (pathURL !== '/' || (pathURL === '/' && admin.tabsBody().html())) {
admin.tabsBodyChange(tabsPage.index)
return reset(tabsPage.type)
}
}
//请求视图渲染
view().render(path.join('/')).then(function (res) {
//遍历页签选项卡
var matchTo
, tabs = $('#Panel_app_tabsheader>li')
tabs.each(function (index) {
var li = $(this)
, layid = li.attr('lay-id')
if (layid === pathURL) {
matchTo = true
tabsPage.index = index
}
})
//如果未在选项卡中匹配到,则追加选项卡
if (setter.pageTabs && pathURL !== '/') {
if (!matchTo) {
$(APP_BODY).append('<div class="layadmin-tabsbody-item layui-show"></div>')
tabsPage.index = tabs.length
element.tabAdd(FILTER_TAB_TBAS, {
title: '<span>' + (res.title || '新标签页') + '</span>'
, id: pathURL
, attr: router.href
})
}
}
this.container = admin.tabsBody(tabsPage.index)
setter.pageTabs || this.container.scrollTop(0) //如果不开启标签页,则跳转时重置滚动条
//定位当前tabs
element.tabChange(FILTER_TAB_TBAS, pathURL)
admin.tabsBodyChange(tabsPage.index)
}).done(function () {
layui.use('common', layui.cache.callback.common)
$win.on('resize', layui.data.resize)
element.render('breadcrumb', 'breadcrumb')
//容器 scroll 事件,剔除吸附层
admin.tabsBody(tabsPage.index).on('scroll', function () {
var othis = $(this)
, elemDate = $('.layui-laydate')
, layerOpen = $('.layui-layer')[0]
//关闭 layDate
if (elemDate[0]) {
elemDate.each(function () {
var thisElemDate = $(this)
thisElemDate.hasClass('layui-laydate-static') || thisElemDate.remove()
})
othis.find('input').blur()
}
//关闭 Tips 层
layerOpen && layer.closeAll('tips')
})
})
reset()
}
//入口页面
var entryPage = function (fn) {
var router = layui.router()
, container = view(setter.container)
, pathURL = admin.correctRouter(router.path.join('/'))
, isIndPage
//检查是否属于独立页面
layui.each(setter.indPage, function (index, item) {
if (pathURL === item) {
return isIndPage = true
}
})
//将模块根路径设置为 modules 目录
layui.config({
base: setter.paths.base + 'modules/'
})
//独立页面
if (isIndPage || pathURL === '/user/login') { //此处单独判断登入页,是为了兼容旧版(即未在 config.js 配置 indPage 的情况)
container.render(router.path.join('/')).done(function () {
admin.pageType = 'alone'
})
} else { //后台框架页面
//强制拦截未登入
if (setter.interceptor) {
var local = layui.data(setter.tableName)
if (!local[setter.request.tokenName]) {
return location.hash = '/user/login/redirect=' + encodeURIComponent(pathURL) //跳转到登入页
}
}
//渲染后台结构
if (admin.pageType === 'console') { //后台主体页
renderPage()
} else { //初始控制台结构
container.render('layout').done(function () {
renderPage()
layui.element.render()
if (admin.screen() < 2) {
admin.sideFlexible()
}
admin.pageType = 'console'
})
}
}
}
var APP_BODY = '#Panel_app_body'
var FILTER_TAB_TBAS = 'layadmin-layout-tabs'
var $ = layui.$, $win = $(window)
//初始主体结构
layui.link(
setter.paths.core + 'css/admin.css?v=' + admin.v
, function () {
entryPage()
}
, 'layuiAdmin'
)
//Hash改变
window.onhashchange = function () {
entryPage()
//执行 {setter.MOD_NAME}.hash 下的事件
layui.event.call(this, setter.MOD_NAME, 'hash({*})', layui.router())
}
// 对外输出
var adminuiIndex = {
render: renderPage
}
$.extend(admin, adminuiIndex)
exports('adminIndex', adminuiIndex)
})

View File

@@ -0,0 +1,346 @@
/**
* 界面视图模块
*/
layui.define(['laytpl', 'layer'], function (exports) {
var $ = layui.jquery
, laytpl = layui.laytpl
, layer = layui.layer
, setter = layui.setter
, device = layui.device()
, hint = layui.hint()
//对外接口
, view = function (id) {
return new Class(id)
}
, SHOW = 'layui-show', LAY_BODY = 'Panel_app_body'
//构造器
, Class = function (id) {
this.id = id
this.container = $('#' + (id || LAY_BODY))
}
//加载中
view.loading = function (elem) {
elem.append(
this.elemLoad = $('<i class="layui-anim layui-anim-rotate layui-anim-loop layui-icon layui-icon-loading layadmin-loading"></i>')
)
}
//移除加载
view.removeLoad = function () {
this.elemLoad && this.elemLoad.remove()
}
//清除 token并跳转到登入页
view.exit = function () {
//清空本地记录的 token
layui.data(setter.tableName, {
key: setter.request.tokenName
, remove: true
})
//跳转到登入页
location.hash = '/user/login'
}
//Ajax请求
view.req = function (options) {
var that = this
, success = options.success
, error = options.error
, request = setter.request
, response = setter.response
, debug = function () {
return setter.debug
? '<br><cite>URL</cite>' + options.url
: ''
}
options.data = options.data || {}
options.headers = options.headers || {}
if (request.tokenName) {
var sendData = typeof options.data === 'string'
? JSON.parse(options.data)
: options.data
//自动给参数传入默认 token
options.data[request.tokenName] = request.tokenName in sendData
? options.data[request.tokenName]
: (layui.data(setter.tableName)[request.tokenName] || '')
//自动给 Request Headers 传入 token
options.headers[request.tokenName] = request.tokenName in options.headers
? options.headers[request.tokenName]
: (layui.data(setter.tableName)[request.tokenName] || '')
}
delete options.success
delete options.error
return $.ajax($.extend({
type: 'get'
, dataType: 'json'
, success: function (res) {
var statusCode = response.statusCode
//只有 response 的 code 一切正常才执行 done
if (res[response.statusName] == statusCode.ok) {
typeof options.done === 'function' && options.done(res)
}
//登录状态失效,清除本地 access_token并强制跳转到登入页
else if (res[response.statusName] == statusCode.logout) {
view.exit()
}
//其它异常
else {
var errorText = [
'<cite>Error</cite> ' + (res[response.msgName] || '返回状态码异常')
, debug()
].join('')
view.error(errorText)
}
//只要 http 状态码正常,无论 response 的 code 是否正常都执行 success
typeof success === 'function' && success(res)
}
, error: function (e, code) {
var errorText = [
'请求异常,请重试<br><cite>错误信息:</cite>' + code
, debug()
].join('')
view.error(errorText)
typeof error === 'function' && error.apply(this, arguments)
}
}, options))
}
//弹窗
view.popup = function (options) {
var success = options.success
, skin = options.skin
delete options.success
delete options.skin
return layer.open($.extend({
type: 1
, title: '提示'
, content: ''
, id: 'LAY-system-view-popup'
, skin: 'layui-layer-admin' + (skin ? ' ' + skin : '')
, shadeClose: true
, closeBtn: false
, success: function (layero, index) {
var elemClose = $('<i class="layui-icon" close>&#x1006;</i>')
layero.append(elemClose)
elemClose.on('click', function () {
layer.close(index)
})
typeof success === 'function' && success.apply(this, arguments)
}
}, options))
}
//异常提示
view.error = function (content, options) {
return view.popup($.extend({
content: content
, maxWidth: 300
//,shade: 0.01
, offset: 't'
, anim: 6
, id: 'LAY_adminError'
}, options))
}
//请求模板文件渲染
Class.prototype.render = function (views, params) {
var that = this, router = layui.router()
views = (
setter.paths && setter.paths.views
? setter.paths.views
: setter.views
) + views + setter.engine
$('#' + LAY_BODY).children('.layadmin-loading').remove()
view.loading(that.container) //loading
//请求模板
$.ajax({
url: views
, type: 'get'
, dataType: 'html'
, data: {
v: layui.cache.version
}
, success: function (html) {
html = '<div>' + html + '</div>'
var elemTitle = $(html).find('title')
, title = elemTitle.text() || (html.match(/\<title\>([\s\S]*)\<\/title>/) || [])[1]
var res = {
title: title
, body: html
}
elemTitle.remove()
that.params = params || {} //获取参数
if (that.then) {
that.then(res)
delete that.then
}
that.parse(html)
view.removeLoad()
if (that.done) {
that.done(res)
delete that.done
}
}
, error: function (e) {
view.removeLoad()
if (that.render.isError) {
return view.error('请求视图文件异常,状态:' + e.status)
}
if (e.status === 404) {
that.render('template/tips/404')
} else {
that.render('template/tips/error')
}
that.render.isError = true
}
})
return that
}
//解析模板
Class.prototype.parse = function (html, refresh, callback) {
var that = this
, isScriptTpl = typeof html === 'object' //是否模板元素
, elem = isScriptTpl ? html : $(html)
, elemTemp = isScriptTpl ? html : elem.find('*[template]')
, fn = function (options) {
var tpl = laytpl(options.dataElem.html())
, res = $.extend({
params: router.params
}, options.res)
options.dataElem.after(tpl.render(res))
typeof callback === 'function' && callback()
try {
options.done && new Function('d', options.done)(res)
} catch (e) {
console.error(options.dataElem[0], '\n存在错误回调脚本\n\n', e)
}
}
, router = layui.router()
elem.find('title').remove()
that.container[refresh ? 'after' : 'html'](elem.children())
router.params = that.params || {}
//遍历模板区块
for (var i = elemTemp.length; i > 0; i--) {
(function () {
var dataElem = elemTemp.eq(i - 1)
, layDone = dataElem.attr('lay-done') || dataElem.attr('lay-then') //获取回调
, url = laytpl(dataElem.attr('lay-url') || '').render(router) //接口 url
, data = laytpl(dataElem.attr('lay-data') || '').render(router) //接口参数
, headers = laytpl(dataElem.attr('lay-headers') || '').render(router) //接口请求的头信息
try {
data = new Function('return ' + data + ';')()
} catch (e) {
hint.error('lay-data: ' + e.message)
data = {}
}
try {
headers = new Function('return ' + headers + ';')()
} catch (e) {
hint.error('lay-headers: ' + e.message)
headers = headers || {}
}
if (url) {
view.req({
type: dataElem.attr('lay-type') || 'get'
, url: url
, data: data
, dataType: 'json'
, headers: headers
, success: function (res) {
fn({
dataElem: dataElem
, res: res
, done: layDone
})
}
})
} else {
fn({
dataElem: dataElem
, done: layDone
})
}
}())
}
return that
}
//直接渲染字符
Class.prototype.send = function (views, data) {
var tpl = laytpl(views || this.container.html()).render(data || {})
this.container.html(tpl)
return this
}
//局部刷新模板
Class.prototype.refresh = function (callback) {
var that = this
, next = that.container.next()
, templateid = next.attr('lay-templateid')
if (that.id != templateid) return that
that.parse(that.container, 'refresh', function () {
that.container.siblings('[lay-templateid="' + that.id + '"]:last').remove()
typeof callback === 'function' && callback()
})
return that
}
//视图请求成功后的回调
Class.prototype.then = function (callback) {
this.then = callback
return this
}
//视图渲染完毕后的回调
Class.prototype.done = function (callback) {
this.done = callback
return this
}
//对外接口
exports('view', view)
})

154
public/res/config.js Normal file
View File

@@ -0,0 +1,154 @@
/**
* setter
*/
// 初始化配置
layui.define(['all'], function (exports) {
exports('setter', {
paths: { // v1.9.0 及以上版本的写法
core: layui.cache.base + 'adminui/src/', // 核心库所在目录
views: layui.cache.base + 'views/', // 业务视图所在目录
modules: layui.cache.base + 'modules/', // 业务模块所在目录
base: layui.cache.base // 记录静态资源所在基础目录
},
container: 'Panel_app', // 容器ID
entry: 'index', // 默认视图文件名
engine: '.html', // 视图文件后缀名
pageTabs: true, // 是否开启页面选项卡功能。单页版不推荐开启
refreshCurrPage: true, // 当跳转页面 url 与当前页 url 相同时,是否自动执行刷新
name: '耗子Linux面板',
tableName: 'HaoZiPanel', // 本地存储表名
MOD_NAME: 'admin', // 模块事件名
debug: true, // 是否开启调试模式。如开启,接口异常时会抛出异常 URL 等信息
interceptor: true, // 是否开启未登入拦截
// 自定义请求字段
request: {
tokenName: 'access_token' // 自动携带 token 的字段名。可设置 false 不携带。
},
// 自定义响应字段
response: {
statusName: 'code', // 数据状态的字段名称
statusCode: {
ok: 0, // 数据状态一切正常的状态码
logout: 1001 // 登录状态失效的状态码
},
msgName: 'message', // 状态信息的字段名称
dataName: 'data' // 数据详情的字段名称
},
// 独立页面路由,可随意添加(无需写参数)
indPage: [
'/user/login', // 登入页
],
// 配置业务模块目录中的特殊模块
extend: {
layim: 'layim/layim' // layim
},
// 主题配置
theme: {
// 内置主题配色方案
color: [{
main: '#20222A', // 主题色
selected: '#16baaa', // 选中色
alias: 'default' // 默认别名
}, {
main: '#03152A',
selected: '#3B91FF',
alias: 'dark-blue' // 藏蓝
}, {
main: '#2E241B',
selected: '#A48566',
alias: 'coffee' // 咖啡
}, {
main: '#50314F',
selected: '#7A4D7B',
alias: 'purple-red' // 紫红
}, {
main: '#344058',
logo: '#1E9FFF',
selected: '#1E9FFF',
alias: 'ocean' // 海洋
}, {
main: '#3A3D49',
logo: '#2F9688',
selected: '#16b777',
alias: 'green' // 墨绿
}, {
main: '#20222A',
logo: '#F78400',
selected: '#F78400',
alias: 'red' // 橙色
}, {
main: '#28333E',
logo: '#AA3130',
selected: '#AA3130',
alias: 'fashion-red' // 时尚红
}, {
main: '#24262F',
logo: '#3A3D49',
selected: '#16baaa',
alias: 'classic-black' // 经典黑
}, {
logo: '#226A62',
header: '#2F9688',
alias: 'green-header' // 墨绿头
}, {
main: '#344058',
logo: '#0085E8',
selected: '#1E9FFF',
header: '#1E9FFF',
alias: 'ocean-header' // 海洋头
}, {
header: '#393D49',
alias: 'classic-black-header' // 经典黑
}, {
main: '#50314F',
logo: '#50314F',
selected: '#7A4D7B',
header: '#50314F',
alias: 'purple-red-header' // 紫红头
}, {
main: '#28333E',
logo: '#28333E',
selected: '#AA3130',
header: '#AA3130',
alias: 'fashion-red-header' // 时尚红头
}, {
main: '#28333E',
logo: '#16baaa',
selected: '#16baaa',
header: '#16baaa',
alias: 'green-header' // 墨绿头
}, {
main: '#393D49',
logo: '#393D49',
selected: '#16baaa',
header: '#23262E',
alias: 'Classic-style1' // 经典风格1
}, {
main: '#001529',
logo: '#001529',
selected: '#1890FF',
header: '#1890FF',
alias: 'Classic-style2' // 经典风格2
}, {
main: '#25282A',
logo: '#25282A',
selected: '#35BDB2',
header: '#35BDB2',
alias: 'Classic-style3' // 经典风格3
}],
// 初始的颜色索引,对应上面的配色方案数组索引
// 如果本地已经有主题色记录则以本地记录为优先除非请求本地数据localStorage
initColorIndex: 0
}
})
})

40
public/res/index.js Normal file
View File

@@ -0,0 +1,40 @@
/**
* 初始化主题入口模块
*/
layui.extend({
setter: 'config' // 将 config.js 扩展到 layui 模块
}).define(['setter'], function (exports) {
var setter = layui.setter
// 将核心库扩展到 layui 模块
layui.each({
admin: 'admin',
view: 'view',
adminIndex: 'index'
}, function (modName, fileName) {
var libs = {}
libs[modName] = '{/}' + setter.paths.core + '/modules/' + fileName
layui.extend(libs)
})
// 指定业务模块基础目录
layui.config({
base: setter.paths.modules
})
// 将业务模块中的特殊模块扩展到 layui 模块
layui.each(setter.extend, function (key, value) {
var mods = {}
mods[key] = '{/}' + layui.cache.base + value
layui.extend(mods)
})
// 加载主题核心库入口模块
layui.use('adminIndex', function () {
layui.use('common') // 加载公共业务模块,如不需要可剔除
// 输出模块 / 模块加载完毕标志
exports('index', layui.admin)
})
})

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
/**
* common
*/
layui.define(function (exports) {
var $ = layui.$
, layer = layui.layer
, laytpl = layui.laytpl
, setter = layui.setter
, view = layui.view
, admin = layui.admin
//公共业务的逻辑处理可以写在此处,切换任何页面都会执行
//……
//退出
admin.events.logout = function () {
//执行退出接口
admin.req({
url: './res/json/user/logout.js'
, type: 'get'
, data: {}
, done: function (res) { //这里要说明一下done 是只有 response 的 code 正常才会执行。而 succese 则是只要 http 为 200 就会执行
//清空本地记录的 token并跳转到登入页
admin.exit()
}
})
}
//对外暴露的接口
exports('common', {})
})

View File

@@ -0,0 +1,330 @@
/**
* console
*/
layui.define(function (exports) {
/*
下面通过 layui.use 分段加载不同的模块,实现不同区域的同时渲染,从而保证视图的快速呈现
*/
//区块轮播切换
layui.use(['admin', 'carousel'], function () {
var $ = layui.$
, admin = layui.admin
, carousel = layui.carousel
, element = layui.element
, device = layui.device()
//轮播切换
$('.layadmin-carousel').each(function () {
var othis = $(this)
carousel.render({
elem: this
, width: '100%'
, arrow: 'none'
, interval: othis.data('interval')
, autoplay: othis.data('autoplay') === true
, trigger: (device.ios || device.android) ? 'click' : 'hover'
, anim: othis.data('anim')
})
})
element.render('progress')
})
//数据概览
layui.use(['admin', 'carousel', 'echarts'], function () {
var $ = layui.$
, admin = layui.admin
, carousel = layui.carousel
, echarts = layui.echarts
var echartsApp = [], options = [
//今日流量趋势
{
title: {
text: '今日流量趋势',
x: 'center',
textStyle: {
fontSize: 14
}
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['', '']
},
xAxis: [{
type: 'category',
boundaryGap: false,
data: ['06:00', '06:30', '07:00', '07:30', '08:00', '08:30', '09:00', '09:30', '10:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30']
}],
yAxis: [{
type: 'value'
}],
series: [{
name: 'PV',
type: 'line',
smooth: true,
itemStyle: { normal: { areaStyle: { type: 'default' } } },
data: [111, 222, 333, 444, 555, 777, 3333, 33333, 55555, 88888, 33333, 3333, 7777, 11888, 28888, 38888, 58888, 42222, 39999, 28888, 17777, 9777, 6555, 5555, 3333, 2222, 3111, 6999, 5888, 2777, 1777, 999, 888, 777]
}, {
name: 'UV',
type: 'line',
smooth: true,
itemStyle: { normal: { areaStyle: { type: 'default' } } },
data: [11, 22, 33, 44, 55, 66, 333, 3333, 5555, 12312, 3333, 333, 777, 1188, 2777, 3888, 7777, 4222, 3999, 2888, 1777, 966, 655, 555, 333, 222, 311, 699, 588, 277, 166, 99, 88, 77]
}]
},
/*
//访客浏览器分布
{
title : {
text: '访客浏览器分布',
x: 'center',
textStyle: {
fontSize: 14
}
},
tooltip : {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
legend: {
orient : 'vertical',
x : 'left',
data:['Chrome','Firefox','IE 8.0','Safari','其它浏览器']
},
series : [{
name:'访问来源',
type:'pie',
radius : '55%',
center: ['50%', '50%'],
data:[
{value:9052, name:'Chrome'},
{value:1610, name:'Firefox'},
{value:3200, name:'IE 8.0'},
{value:535, name:'Safari'},
{value:1700, name:'其它浏览器'}
]
}]
},
*/
//新增的用户量
{
title: {
text: '最近一周新增的用户量',
x: 'center',
textStyle: {
fontSize: 14
}
},
tooltip: { //提示框
trigger: 'axis',
formatter: '{b}<br>新增用户:{c}'
},
xAxis: [{ //X轴
type: 'category',
data: ['11-07', '11-08', '11-09', '11-10', '11-11', '11-12', '11-13']
}],
yAxis: [{ //Y轴
type: 'value'
}],
series: [{ //内容
type: 'line',
data: [200, 300, 400, 610, 150, 270, 380],
}]
}
]
, elemDataView = $('#LAY-index-dataview').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme)
echartsApp[index].setOption(options[index])
//window.onresize = echartsApp[index].resize;
admin.resize(function () {
echartsApp[index].resize()
})
}
//没找到DOM终止执行
if (!elemDataView[0]) return
renderDataView(0)
//触发数据概览轮播
var carouselIndex = 0
carousel.on('change(LAY-index-dataview)', function (obj) {
renderDataView(carouselIndex = obj.index)
})
//触发侧边伸缩
layui.admin.on('side', function () {
setTimeout(function () {
renderDataView(carouselIndex)
}, 300)
})
//触发路由
layui.admin.on('hash(tab)', function () {
layui.router().path.join('') || renderDataView(carouselIndex)
})
})
//地图
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts
var echartsApp = [], options = [
{
title: {
text: '访客地区分布',
subtext: '不完全统计'
},
tooltip: {
trigger: 'item'
},
dataRange: {
orient: 'horizontal',
min: 0,
max: 60000,
text: ['高', '低'],
splitNumber: 0
},
series: [
{
name: '访客地区分布',
type: 'map',
mapType: 'china',
selectedMode: 'multiple',
itemStyle: {
normal: { label: { show: true } },
emphasis: { label: { show: true } }
},
data: [
{ name: '西藏', value: 60 },
{ name: '青海', value: 167 },
{ name: '宁夏', value: 210 },
{ name: '海南', value: 252 },
{ name: '甘肃', value: 502 },
{ name: '贵州', value: 570 },
{ name: '新疆', value: 661 },
{ name: '云南', value: 8890 },
{ name: '重庆', value: 10010 },
{ name: '吉林', value: 5056 },
{ name: '山西', value: 2123 },
{ name: '天津', value: 9130 },
{ name: '江西', value: 10170 },
{ name: '广西', value: 6172 },
{ name: '陕西', value: 9251 },
{ name: '黑龙江', value: 5125 },
{ name: '内蒙古', value: 1435 },
{ name: '安徽', value: 9530 },
{ name: '北京', value: 51919 },
{ name: '福建', value: 3756 },
{ name: '上海', value: 59190 },
{ name: '湖北', value: 37109 },
{ name: '湖南', value: 8966 },
{ name: '四川', value: 31020 },
{ name: '辽宁', value: 7222 },
{ name: '河北', value: 3451 },
{ name: '河南', value: 9693 },
{ name: '浙江', value: 62310 },
{ name: '山东', value: 39231 },
{ name: '江苏', value: 35911 },
{ name: '广东', value: 55891 }
]
}
]
}
]
, elemDataView = $('#LAY-index-pagethree-home').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme)
echartsApp[index].setOption(options[index])
window.onresize = echartsApp[index].resize
}
//没找到DOM终止执行
if (!elemDataView[0]) return
renderDataView(0)
})
//table
layui.use('table', function () {
var $ = layui.$
, table = layui.table
//今日热搜
table.render({
elem: '#LAY-index-topSearch'
, url: './res/json/console/top-search.js' //模拟接口
, page: true
, cols: [[
{ type: 'numbers', fixed: 'left' }
, {
field: 'keywords',
title: '关键词',
minWidth: 300,
templet: '<div><a href="https://www.baidu.com/s?wd={{ d.keywords }}" target="_blank" class="layui-table-link">{{ d.keywords }}</div>'
}
, { field: 'frequency', title: '搜索次数', minWidth: 120, sort: true }
, { field: 'userNums', title: '用户数', sort: true }
]]
, skin: 'line'
})
//今日热贴
table.render({
elem: '#LAY-index-topCard'
, url: './res/json/console/top-card.js' //模拟接口
, page: true
, cellMinWidth: 120
, cols: [[
{ type: 'numbers', fixed: 'left' }
, {
field: 'title',
title: '标题',
minWidth: 300,
templet: '<div><a href="{{ d.href }}" target="_blank" class="layui-table-link">{{ d.title }}</div>'
}
, { field: 'username', title: '发帖者' }
, { field: 'channel', title: '类别' }
, { field: 'crt', title: '点击率', sort: true }
]]
, skin: 'line'
})
//项目进展
table.render({
elem: '#LAY-home-homepage-console'
, url: './res/json/console/prograss.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'prograss', title: '任务' }
, { field: 'time', title: '所需时间' }
, {
field: 'complete', title: '完成情况'
, templet: function (d) {
if (d.complete == '已完成') {
return '<del style="color: #16b777;">' + d.complete + '</del>'
} else if (d.complete == '进行中') {
return '<span style="color: #FFB800;">' + d.complete + '</span>'
} else {
return '<span style="color: #FF5722;">' + d.complete + '</span>'
}
}
}
]]
, skin: 'line'
})
})
exports('console', {})
})

View File

@@ -0,0 +1,160 @@
/**
* 内容系统 demo
*/
layui.define(['table', 'form'], function (exports) {
var $ = layui.$
, admin = layui.admin
, view = layui.view
, table = layui.table
, form = layui.form
//文章管理
table.render({
elem: '#LAY-app-content-list'
, url: './res/json/content/list.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', width: 100, title: '文章ID', sort: true }
, { field: 'label', title: '文章标签', minWidth: 100 }
, { field: 'title', title: '文章标题' }
, { field: 'author', title: '作者' }
, { field: 'uploadtime', title: '上传时间', sort: true }
, { field: 'status', title: '发布状态', templet: '#buttonTpl', minWidth: 80, align: 'center' }
, { title: '操作', minWidth: 150, align: 'center', fixed: 'right', toolbar: '#table-content-list' }
]]
, page: true
, limit: 10
, limits: [10, 15, 20, 25, 30]
, text: '对不起,加载出现异常!'
})
//工具条
table.on('tool(LAY-app-content-list)', function (obj) {
var data = obj.data
if (obj.event === 'del') {
layer.confirm('确定删除此文章?', function (index) {
obj.del()
layer.close(index)
})
} else if (obj.event === 'edit') {
admin.popup({
title: '编辑文章'
, area: ['550px', '550px']
, id: 'LAY-popup-content-edit'
, success: function (layero, index) {
view(this.id).render('app/content/listform', data).done(function () {
form.render(null, 'layuiadmin-app-form-list')
//提交
form.on('submit(layuiadmin-app-form-submit)', function (data) {
var field = data.field //获取提交的字段
//提交 Ajax 成功后,关闭当前弹层并重载表格
//$.ajax({});
layui.table.reload('LAY-app-content-list') //重载表格
layer.close(index) //执行关闭
})
})
}
})
}
})
//分类管理
table.render({
elem: '#LAY-app-content-tags'
, url: './res/json/content/tags.js' //模拟接口
, cols: [[
{ type: 'numbers', fixed: 'left' }
, { field: 'id', width: 100, title: 'ID', sort: true }
, { field: 'tags', title: '分类名', minWidth: 100 }
, { title: '操作', width: 150, align: 'center', fixed: 'right', toolbar: '#layuiadmin-app-cont-tagsbar' }
]]
, text: '对不起,加载出现异常!'
})
//工具条
table.on('tool(LAY-app-content-tags)', function (obj) {
var data = obj.data
if (obj.event === 'del') {
layer.confirm('确定删除此分类?', function (index) {
obj.del()
layer.close(index)
})
} else if (obj.event === 'edit') {
admin.popup({
title: '编辑分类'
, area: ['450px', '200px']
, id: 'LAY-popup-content-tags'
, success: function (layero, index) {
view(this.id).render('app/content/tagsform', data).done(function () {
form.render(null, 'layuiadmin-form-tags')
//提交
form.on('submit(layuiadmin-app-tags-submit)', function (data) {
var field = data.field //获取提交的字段
//提交 Ajax 成功后,关闭当前弹层并重载表格
//$.ajax({});
layui.table.reload('LAY-app-content-tags') //重载表格
layer.close(index) //执行关闭
})
})
}
})
}
})
//评论管理
table.render({
elem: '#LAY-app-content-comm'
, url: './res/json/content/comment.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', width: 100, title: 'ID', sort: true }
, { field: 'reviewers', title: '评论者', minWidth: 100 }
, { field: 'content', title: '评论内容', minWidth: 100 }
, { field: 'commtime', title: '评论时间', minWidth: 100, sort: true }
, { title: '操作', width: 150, align: 'center', fixed: 'right', toolbar: '#table-content-com' }
]]
, page: true
, limit: 10
, limits: [10, 15, 20, 25, 30]
, text: '对不起,加载出现异常!'
})
//工具条
table.on('tool(LAY-app-content-comm)', function (obj) {
var data = obj.data
if (obj.event === 'del') {
layer.confirm('确定删除此条评论?', function (index) {
obj.del()
layer.close(index)
})
} else if (obj.event === 'edit') {
admin.popup({
title: '编辑评论'
, area: ['450px', '300px']
, id: 'LAY-popup-content-comm'
, success: function (layero, index) {
view(this.id).render('app/content/contform', data).done(function () {
form.render(null, 'layuiadmin-form-comment')
//提交
form.on('submit(layuiadmin-app-com-submit)', function (data) {
var field = data.field //获取提交的字段
//提交 Ajax 成功后,关闭当前弹层并重载表格
//$.ajax({});
layui.table.reload('LAY-app-content-comm') //重载表格
layer.close(index) //执行关闭
})
})
}
})
}
})
exports('contlist', {})
})

21411
public/res/modules/echarts.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
/**
* Set echarts theme
*/
layui.define(function (exports) {
exports('echartsTheme', {
// 默认色板
color: [
'#16baaa', '#1E9FFF', '#16b777', '#FFB980', '#D87A80',
'#8d98b3', '#e5cf0d', '#97b552', '#95706d', '#dc69aa',
'#07a2a4', '#9a7fd1', '#588dd5', '#f5994e', '#c05050',
'#59678c', '#c9ab00', '#7eb00a', '#6f5553', '#c14089'
],
// 图表标题
title: {
textStyle: {
fontWeight: 'normal',
color: '#5F5F5F' // 主标题文字颜色
}
},
// 值域
dataRange: {
itemWidth: 15,
color: ['#16baaa', '#e0ffff']
},
// 工具箱
toolbox: {
color: ['#1e90ff', '#1e90ff', '#1e90ff', '#1e90ff'],
effectiveColor: '#ff4500'
},
// 提示框
tooltip: {
backgroundColor: 'rgba(50,50,50,0.5)', // 提示背景颜色默认为透明度为0.7的黑色
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'line', // 默认为直线,可选为:'line' | 'shadow'
lineStyle: { // 直线指示器样式设置
color: '#16baaa'
},
crossStyle: {
color: '#008acd'
},
shadowStyle: { // 阴影指示器样式设置
color: 'rgba(200,200,200,0.2)'
}
}
},
// 区域缩放控制器
dataZoom: {
dataBackgroundColor: '#efefff', // 数据背景颜色
fillerColor: 'rgba(182,162,222,0.2)', // 填充颜色
handleColor: '#008acd' // 手柄颜色
},
// 网格
grid: {
borderColor: '#eee'
},
// 类目轴 - X轴
categoryAxis: {
axisLine: { // 坐标轴线
lineStyle: { // 属性lineStyle控制线条样式
color: '#16baaa'
}
},
axisTick: { //小标记
show: false
},
splitLine: { // 分隔线
lineStyle: { // 属性lineStyle详见lineStyle控制线条样式
color: ['#eee']
}
}
},
// 数值型坐标轴默认参数 - Y轴
valueAxis: {
axisLine: { // 坐标轴线
lineStyle: { // 属性lineStyle控制线条样式
color: '#16baaa'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: { // 分隔线
lineStyle: { // 属性lineStyle详见lineStyle控制线条样式
color: ['#eee']
}
}
},
polar: {
axisLine: { // 坐标轴线
lineStyle: { // 属性lineStyle控制线条样式
color: '#ddd'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.2)', 'rgba(200,200,200,0.2)']
}
},
splitLine: {
lineStyle: {
color: '#ddd'
}
}
},
timeline: {
lineStyle: {
color: '#16baaa'
},
controlStyle: {
normal: { color: '#16baaa' },
emphasis: { color: '#16baaa' }
},
symbol: 'emptyCircle',
symbolSize: 3
},
// 柱形图默认参数
bar: {
itemStyle: {
normal: {
barBorderRadius: 2
},
emphasis: {
barBorderRadius: 2
}
}
},
// 折线图默认参数
line: {
smooth: true,
symbol: 'emptyCircle', // 拐点图形类型
symbolSize: 3 // 拐点图形大小
},
// K线图默认参数
k: {
itemStyle: {
normal: {
color: '#d87a80', // 阳线填充颜色
color0: '#2ec7c9', // 阴线填充颜色
lineStyle: {
color: '#d87a80', // 阳线边框颜色
color0: '#2ec7c9' // 阴线边框颜色
}
}
}
},
// 散点图默认参数
scatter: {
symbol: 'circle', // 图形类型
symbolSize: 4 // 图形大小半宽半径参数当图形为方向或菱形则总宽度为symbolSize * 2
},
// 雷达图默认参数
radar: {
symbol: 'emptyCircle', // 图形类型
symbolSize: 3
//symbol: null, // 拐点图形类型
//symbolRotate : null, // 图形旋转控制
},
map: {
itemStyle: {
normal: {
areaStyle: {
color: '#ddd'
},
label: {
textStyle: {
color: '#d87a80'
}
}
},
emphasis: { // 也是选中样式
areaStyle: {
color: '#fe994e'
}
}
}
},
force: {
itemStyle: {
normal: {
linkStyle: {
color: '#1e90ff'
}
}
}
},
chord: {
itemStyle: {
normal: {
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)',
chordStyle: {
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
}
}
},
emphasis: {
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)',
chordStyle: {
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
}
}
}
}
},
gauge: {
axisLine: { // 坐标轴线
lineStyle: { // 属性lineStyle控制线条样式
color: [[0.2, '#2ec7c9'], [0.8, '#5ab1ef'], [1, '#d87a80']],
width: 10
}
},
axisTick: { // 坐标轴小标记
splitNumber: 10, // 每份split细分多少段
length: 15, // 属性length控制线长
lineStyle: { // 属性lineStyle控制线条样式
color: 'auto'
}
},
splitLine: { // 分隔线
length: 22, // 属性length控制线长
lineStyle: { // 属性lineStyle详见lineStyle控制线条样式
color: 'auto'
}
},
pointer: {
width: 5
}
},
textStyle: {
fontFamily: '微软雅黑, Arial, Verdana, sans-serif'
}
})
})

119
public/res/modules/forum.js Normal file
View File

@@ -0,0 +1,119 @@
/**
* forum demo
*/
layui.define(['table', 'form'], function (exports) {
var $ = layui.$
, admin = layui.admin
, view = layui.view
, table = layui.table
, form = layui.form
//帖子管理
table.render({
elem: '#LAY-app-forum-list'
, url: './res/json/forum/list.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', width: 100, title: 'ID', sort: true }
, { field: 'poster', title: '发帖人' }
, { field: 'avatar', title: '头像', width: 100, templet: '#imgTpl' }
, { field: 'content', title: '发帖内容' }
, { field: 'posttime', title: '发帖时间', sort: true }
, { field: 'top', title: '置顶', templet: '#buttonTpl', minWidth: 80, align: 'center' }
, { title: '操作', width: 150, align: 'center', fixed: 'right', toolbar: '#table-forum-list' }
]]
, page: true
, limit: 10
, limits: [10, 15, 20, 25, 30]
, text: '对不起,加载出现异常!'
})
//工具条
table.on('tool(LAY-app-forum-list)', function (obj) {
var data = obj.data
if (obj.event === 'del') {
layer.confirm('确定删除此条帖子?', function (index) {
obj.del()
layer.close(index)
})
} else if (obj.event === 'edit') {
admin.popup({
title: '编辑帖子'
, area: ['550px', '450px']
, id: 'LAY-popup-forum-edit'
, resize: false
, success: function (layero, index) {
view(this.id).render('app/forum/listform', data).done(function () {
form.render(null, 'layuiadmin-form-list')
//提交
form.on('submit(layuiadmin-app-forum-submit)', function (data) {
var field = data.field //获取提交的字段
//提交 Ajax 成功后,关闭当前弹层并重载表格
//$.ajax({});
layui.table.reload('LAY-app-forum-list') //重载表格
layer.close(index) //执行关闭
})
})
}
})
}
})
//回帖管理
table.render({
elem: '#LAY-app-forumreply-list'
, url: './res/json/forum/replys.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', width: 100, title: 'ID', sort: true }
, { field: 'replyer', title: '回帖人' }
, { field: 'cardid', title: '回帖ID', sort: true }
, { field: 'avatar', title: '头像', width: 100, templet: '#imgTpl' }
, { field: 'content', title: '回帖内容', width: 200 }
, { field: 'replytime', title: '回帖时间', sort: true }
, { title: '操作', width: 150, align: 'center', fixed: 'right', toolbar: '#table-forum-replys' }
]]
, page: true
, limit: 10
, limits: [10, 15, 20, 25, 30]
, text: '对不起,加载出现异常!'
})
//工具条
table.on('tool(LAY-app-forumreply-list)', function (obj) {
var data = obj.data
if (obj.event === 'del') {
layer.confirm('确定删除此条评论?', function (index) {
obj.del()
layer.close(index)
})
} else if (obj.event === 'edit') {
admin.popup({
title: '编辑回帖'
, area: ['550px', '400px']
, id: 'LAY-popup-forum-edit'
, resize: false
, success: function (layero, index) {
view(this.id).render('app/forum/replysform', data).done(function () {
form.render(null, 'layuiadmin-app-forum-reply')
//提交
form.on('submit(layuiadmin-app-forumreply-submit)', function (data) {
var field = data.field //获取提交的字段
//提交 Ajax 成功后,关闭当前弹层并重载表格
//$.ajax({});
layui.table.reload('LAY-app-forumreply-list') //重载表格
layer.close(index) //执行关闭
})
})
}
})
}
})
exports('forum', {})
})

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>演示聊天记录模板</title>
<link rel="stylesheet" href="//unpkg.com/layui@2.6.8/dist/css/layui.css">
<style>
html {
background-color: #f5f5f5;
}
body .layim-chat-main {
height: auto;
}
</style>
</head>
<body>
<div class="layim-chat-main">
<ul id="LAY_view"></ul>
</div>
<div id="LAY_page" style="margin: 0 10px;"></div>
<textarea title="消息模版" id="LAY_tpl" style="display:none;">
{{# layui.each(d.data, function(index, item){
if(item.id == layui.layim.cache().mine.id){ }}
<li class="layim-chat-mine"><div class="layim-chat-user"><img src="{{ item.avatar }}"><cite><i>{{ layui.data.date(item.timestamp) }}</i>{{ item.username }}</cite></div><div class="layim-chat-text">{{ layui.layim.content(item.content) }}</div></li>
{{# } else { }}
<li><div class="layim-chat-user"><img src="{{ item.avatar }}"><cite>{{ item.username }}<i>{{ layui.data.date(item.timestamp) }}</i></cite></div><div class="layim-chat-text">{{ layui.layim.content(item.content) }}</div></li>
{{# }
}); }}
</textarea>
<!--
上述模版采用了 laytpl 语法
-->
<script src="//unpkg.com/layui@2.6.8/dist/layui.js"></script>
<script>
layui.link('../layim.css', 'skinlayimcss') //加载 css
layui.config({
layimPath: '../../' //配置 layim.js 所在目录
, layimResPath: '../' //layim 资源文件所在目录
}).use(['jquery'], function () {
var layim = parent.layui.layim
, laytpl = parent.layui.laytpl
, $ = layui.jquery
, laypage = parent.layui.laypage
//聊天记录的分页此处不做演示,你可以采用 laypage
//开始请求聊天记录
var param = location.search //获得URL参数。该窗口url会携带会话id和type他们是你请求聊天记录的重要凭据
//实际使用时下述的res一般是通过Ajax获得而此处仅仅只是演示数据格式
, res = {
code: 0
, msg: ''
, data: [{
username: '我'
, id: 100000
, avatar: '' || layui.cache.layimResPath + 'images/default.png'
, timestamp: 1480897882000
, content: '我方模拟记录 111'
}, {
username: 'test123'
, id: 108101
, avatar: '' || layui.cache.layimResPath + 'images/default.png'
, timestamp: 1480897892000
, content: '对方模拟记录 111'
}, {
username: 'test123'
, id: 108101
, avatar: '' || layui.cache.layimResPath + 'images/default.png'
, timestamp: 1480897898000
, content: '对方模拟记录 222'
}, {
username: 'test123'
, id: 108101
, avatar: '' || layui.cache.layimResPath + 'images/default.png'
, timestamp: 1480897908000
, content: '注意:该页面为 chatLog 参数指向的自定义页面。此页仅为聊天记录的模拟数据,实际使用时请进行相应修改。'
}]
}
//console.log(param)
var html = laytpl(LAY_tpl.value).render({
data: res.data
})
$('#LAY_view').html(html)
})
</script>
</body>
</html>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>发现</title>
<link rel="stylesheet" href="//unpkg.com/layui@2.6.8/dist/css/layui.css">
<style>
</style>
</head>
<body>
<div style="margin: 15px;">
<blockquote class="layui-elem-quote">
通过 find 参数指向的自定义页面
</blockquote>
</div>
<script src="//unpkg.com/layui@2.6.8/dist/layui.js"></script>
<script>
layui.config({
layimPath: '../../' //配置 layim.js 所在目录
, layimResPath: '../' //layim 资源文件所在目录
}).extend({
layim: layui.cache.layimPath + 'layim' //配置 layim 组件所在的路径
}).use(['layim', 'laypage'], function () {
var layim = layui.layim
, layer = layui.layer
, laytpl = layui.laytpl
, $ = layui.jquery
, laypage = layui.laypage
//一些添加好友请求之类的交互参见文档
})
</script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
{
"code": 0,
"pages": 1,
"data": [
{
"id": 76,
"content": "申请添加你为好友",
"uid": 168,
"from": 166488,
"from_group": 0,
"type": 1,
"remark": "test1",
"href": null,
"read": 1,
"time": "刚刚",
"user": {
"id": 166488,
"avatar": "http://q.qlogo.cn/qqapp/101235792/B704597964F9BD0DB648292D1B09F7E8/100",
"username": "测试A",
"sign": null
}
},
{
"id": 75,
"content": "申请添加你为好友",
"uid": 168,
"from": 347592,
"from_group": 0,
"type": 1,
"remark": "test2",
"href": null,
"read": 1,
"time": "刚刚",
"user": {
"id": 347592,
"avatar": "http://q.qlogo.cn/qqapp/101235792/B78751375E0531675B1272AD994BA875/100",
"username": "测试B",
"sign": null
}
},
{
"id": 62,
"content": "测试C 拒绝了你的好友申请",
"uid": 168,
"from": null,
"from_group": null,
"type": 1,
"remark": null,
"href": null,
"read": 1,
"time": "10天前",
"user": {
"id": null
}
},
{
"id": 60,
"content": "测试D 已经同意你的好友申请",
"uid": 168,
"from": null,
"from_group": null,
"type": 1,
"remark": null,
"href": null,
"read": 1,
"time": "10天前",
"user": {
"id": null
}
}
]
}

View File

@@ -0,0 +1,267 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>消息盒子</title>
<link rel="stylesheet" href="//unpkg.com/layui@2.6.8/dist/css/layui.css">
<style>
.layim-msgbox {
margin: 15px;
}
.layim-msgbox li {
position: relative;
margin-bottom: 10px;
padding: 0 130px 10px 60px;
padding-bottom: 10px;
line-height: 22px;
border-bottom: 1px dotted #e2e2e2;
}
.layim-msgbox .layim-msgbox-tips {
margin: 0;
padding: 10px 0;
border: none;
text-align: center;
color: #999;
}
.layim-msgbox .layim-msgbox-system {
padding: 0 10px 10px 10px;
}
.layim-msgbox li p span {
padding-left: 5px;
color: #999;
}
.layim-msgbox li p em {
font-style: normal;
color: #FF5722;
}
.layim-msgbox-avatar {
position: absolute;
left: 0;
top: 0;
width: 50px;
height: 50px;
}
.layim-msgbox-user {
padding-top: 5px;
}
.layim-msgbox-content {
margin-top: 3px;
}
.layim-msgbox .layui-btn-small {
padding: 0 15px;
margin-left: 5px;
}
.layim-msgbox-btn {
position: absolute;
right: 0;
top: 12px;
color: #999;
}
</style>
</head>
<body>
<ul class="layim-msgbox" id="LAY_view"></ul>
<div style="margin: 0 15px;">
<blockquote class="layui-elem-quote">
注意:该页面为 msgbox 参数指向的自定义页面。
<br> 此页为消息盒子的模拟数据,实际使用时请进行相应修改。
</blockquote>
</div>
<textarea title="消息模版" id="LAY_tpl" style="display:none;">
{{# layui.each(d.data, function(index, item){
if(item.from){ }}
<li data-uid="{{ item.from }}" data-fromGroup="{{ item.from_group }}">
<a href="/u/{{ item.from }}/" target="_blank">
<img src="{{ item.user.avatar }}" class="layui-circle layim-msgbox-avatar">
</a>
<p class="layim-msgbox-user">
<a href="/u/{{ item.from }}/" target="_blank">{{ item.user.username||'' }}</a>
<span>{{ item.time }}</span>
</p>
<p class="layim-msgbox-content">
{{ item.content }}
<span>{{ item.remark ? '附言: '+item.remark : '' }}</span>
</p>
<p class="layim-msgbox-btn">
<button class="layui-btn layui-btn-small" data-type="agree">同意</button>
<button class="layui-btn layui-btn-small layui-btn-primary" data-type="refuse">拒绝</button>
</p>
</li>
{{# } else { }}
<li class="layim-msgbox-system">
<p><em>系统:</em>{{ item.content }}<span>{{ item.time }}</span></p>
</li>
{{# }
}); }}
</textarea>
<!--
上述模版采用了 laytpl 语法
-->
<script src="//unpkg.com/layui@2.6.8/dist/layui.js"></script>
<script>
layui.config({
layimPath: '../../' //配置 layim.js 所在目录
, layimResPath: '../' //layim 资源文件所在目录
}).extend({
layim: layui.cache.layimPath + 'layim' //配置 layim 组件所在的路径
}).use(['layim', 'flow'], function () {
var layim = layui.layim
, layer = layui.layer
, laytpl = layui.laytpl
, $ = layui.jquery
, flow = layui.flow
var cache = {} //用于临时记录请求到的数据
//请求消息
var renderMsg = function (page, callback) {
//实际部署时,请将下述 getmsg.json 改为你的接口地址
$.get('getmsg.json', {
page: page || 1
}, function (res) {
if (res.code != 0) {
return layer.msg(res.msg)
}
//记录来源用户信息
layui.each(res.data, function (index, item) {
cache[item.from] = item.user
})
callback && callback(res.data, res.pages)
})
}
//消息信息流
flow.load({
elem: '#LAY_view' //流加载容器
, isAuto: false
, end: '<li class="layim-msgbox-tips">暂无更多新消息</li>'
, done: function (page, next) { //加载下一页
renderMsg(page, function (data, pages) {
var html = laytpl(LAY_tpl.value).render({
data: data
, page: page
})
next(html, page < pages)
})
}
})
//打开页面即把消息标记为已读
/*
$.post('/message/read', {
type: 1
});
*/
//操作
var active = {
//同意
agree: function (othis) {
var li = othis.parents('li')
, uid = li.data('uid')
, from_group = li.data('fromGroup')
, user = cache[uid]
//选择分组
parent.layui.layim.setFriendGroup({
type: 'friend'
, username: user.username
, avatar: user.avatar
, group: parent.layui.layim.cache().friend //获取好友分组数据
, submit: function (group, index) {
//将好友追加到主面板
parent.layui.layim.addList({
type: 'friend'
, avatar: user.avatar //好友头像
, username: user.username //好友昵称
, groupid: group //所在的分组id
, id: uid //好友ID
, sign: user.sign //好友签名
})
parent.layer.close(index)
othis.parent().html('已同意')
//实际部署时,请开启下述注释,并改成你的接口地址
/*
$.post('/im/agreeFriend', {
uid: uid //对方用户ID
,from_group: from_group //对方设定的好友分组
,group: group //我设定的好友分组
}, function(res){
if(res.code != 0){
return layer.msg(res.msg);
}
//将好友追加到主面板
parent.layui.layim.addList({
type: 'friend'
,avatar: user.avatar //好友头像
,username: user.username //好友昵称
,groupid: group //所在的分组id
,id: uid //好友ID
,sign: user.sign //好友签名
});
parent.layer.close(index);
othis.parent().html('已同意');
});
*/
}
})
}
//拒绝
, refuse: function (othis) {
var li = othis.parents('li')
, uid = li.data('uid')
layer.confirm('确定拒绝吗?', function (index) {
layer.close(index)
othis.parent().html('<em>已拒绝</em>')
/*
$.post('/im/refuseFriend', {
uid: uid //对方用户ID
}, function(res){
if(res.code != 0){
return layer.msg(res.msg);
}
layer.close(index);
othis.parent().html('<em>已拒绝</em>');
});
*/
})
}
}
$('body').on('click', '.layui-btn', function () {
var othis = $(this), type = othis.data('type')
active[type] ? active[type].call(this, othis) : ''
})
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show More