This commit is contained in:
meilin.huang
2020-09-01 10:34:11 +08:00
parent 104482ceff
commit 6f0d87c562
103 changed files with 19717 additions and 0 deletions

41
base/assert.go Normal file
View File

@@ -0,0 +1,41 @@
package base
import (
"reflect"
)
func BizErrIsNil(err error, msg string) {
if err != nil {
panic(NewBizErr(msg))
}
}
func ErrIsNil(err error, msg string) {
if err != nil {
panic(err)
}
}
func IsTrue(exp bool, msg string) {
if !exp {
panic(NewBizErr(msg))
}
}
func NotEmpty(str string, msg string) {
if str == "" {
panic(NewBizErr(msg))
}
}
func NotNil(data interface{}, msg string) {
if reflect.ValueOf(data).IsNil() {
panic(NewBizErr(msg))
}
}
func Nil(data interface{}, msg string) {
if !reflect.ValueOf(data).IsNil() {
panic(NewBizErr(msg))
}
}

27
base/bizerror.go Normal file
View File

@@ -0,0 +1,27 @@
package base
// 业务错误
type BizError struct {
code int16
err string
}
// 错误消息
func (e *BizError) Error() string {
return e.err
}
// 错误码
func (e *BizError) Code() int16 {
return e.code
}
// 创建业务逻辑错误结构体,默认为业务逻辑错误
func NewBizErr(msg string) BizError {
return BizError{code: BizErrorCode, err: msg}
}
// 创建业务逻辑错误结构体可设置指定错误code
func NewBizErrCode(code int16, msg string) BizError {
return BizError{code: code, err: msg}
}

141
base/controller.go Normal file
View File

@@ -0,0 +1,141 @@
package base
import (
"encoding/json"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
"github.com/astaxie/beego/validation"
)
type Controller struct {
beego.Controller
}
// 获取数据函数
type getDataFunc func(loginAccount *LoginAccount) interface{}
// 操作函数,无返回数据
type operationFunc func(loginAccount *LoginAccount)
// 将请求体的json赋值给指定的结构体
func (c *Controller) UnmarshalBody(data interface{}) {
err := json.Unmarshal(c.Ctx.Input.RequestBody, data)
BizErrIsNil(err, "request body解析错误")
}
// 校验表单数据
func (c *Controller) validForm(form interface{}) {
valid := validation.Validation{}
b, err := valid.Valid(form)
if err != nil {
panic(err)
}
if !b {
e := valid.Errors[0]
panic(NewBizErr(e.Field + " " + e.Message))
}
}
// 将请求体的json赋值给指定的结构体并校验表单数据
func (c *Controller) UnmarshalBodyAndValid(data interface{}) {
c.UnmarshalBody(data)
c.validForm(data)
}
// 返回数据
// @param checkToken 是否校验token
// @param getData 获取数据的回调函数
func (c *Controller) ReturnData(checkToken bool, getData getDataFunc) {
defer func() {
if err := recover(); err != nil {
c.parseErr(err)
}
}()
var loginAccount *LoginAccount
if checkToken {
loginAccount = c.CheckToken()
}
c.Success(getData(loginAccount))
}
// 无返回数据的操作,如新增修改等无需返回数据的操作
// @param checkToken 是否校验token
func (c *Controller) Operation(checkToken bool, operation operationFunc) {
defer func() {
if err := recover(); err != nil {
c.parseErr(err)
}
}()
var loginAccount *LoginAccount
if checkToken {
loginAccount = c.CheckToken()
}
operation(loginAccount)
c.SuccessNoData()
}
// 校验token并返回登录者账号信息
func (c *Controller) CheckToken() *LoginAccount {
tokenStr := c.Ctx.Input.Header("Authorization")
loginAccount, err := ParseToken(tokenStr)
if err != nil || loginAccount == nil {
panic(NewBizErrCode(TokenErrorCode, TokenErrorMsg))
}
return loginAccount
}
// 获取分页参数
func (c *Controller) GetPageParam() *PageParam {
pn, err := c.GetInt("pageNum", 1)
BizErrIsNil(err, "pageNum参数错误")
ps, serr := c.GetInt("pageSize", 10)
BizErrIsNil(serr, "pageSize参数错误")
return &PageParam{PageNum: pn, PageSize: ps}
}
// 统一返回Result json对象
func (c *Controller) Result(result *Result) {
c.Data["json"] = result
c.ServeJSON()
}
// 返回成功结果
func (c *Controller) Success(data interface{}) {
c.Result(Success(data))
}
// 返回成功结果
func (c *Controller) SuccessNoData() {
c.Result(SuccessNoData())
}
// 返回业务错误
func (c *Controller) BizError(bizError BizError) {
c.Result(Error(bizError.Code(), bizError.Error()))
}
// 返回服务器错误结果
func (c *Controller) ServerError() {
c.Result(ServerError())
}
// 解析error并对不同error返回不同result
func (c *Controller) parseErr(err interface{}) {
switch t := err.(type) {
case BizError:
c.BizError(t)
break
case error:
c.ServerError()
logs.Error(t)
panic(err)
//break
case string:
c.ServerError()
logs.Error(t)
panic(err)
//break
default:
logs.Error(t)
}
}

View File

@@ -0,0 +1,161 @@
package httpclient
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
)
// 默认超时
const DefTimeout = 60
type RequestWrapper struct {
url string
method string
timeout int
body io.Reader
header map[string]string
}
// 创建一个请求
func NewRequest(url string) *RequestWrapper {
return &RequestWrapper{url: url}
}
func (r *RequestWrapper) Url(url string) *RequestWrapper {
r.url = url
return r
}
func (r *RequestWrapper) Timeout(timeout int) *RequestWrapper {
r.timeout = timeout
return r
}
func (r *RequestWrapper) GetByParam(paramMap map[string]string) ResponseWrapper {
var params string
for k, v := range paramMap {
if params != "" {
params += "&"
} else {
params += "?"
}
params += k + "=" + v
}
r.url += "?" + params
return r.Get()
}
func (r *RequestWrapper) Get() ResponseWrapper {
r.method = "GET"
r.body = nil
return request(r)
}
func (r *RequestWrapper) PostJson(body string) ResponseWrapper {
buf := bytes.NewBufferString(body)
r.method = "POST"
r.body = buf
if r.header == nil {
r.header = make(map[string]string)
}
r.header["Content-type"] = "application/json"
return request(r)
}
func (r *RequestWrapper) PostObj(body interface{}) ResponseWrapper {
marshal, err := json.Marshal(body)
if err != nil {
return createRequestError(errors.New("解析json obj错误"))
}
return r.PostJson(string(marshal))
}
func (r *RequestWrapper) PostParams(params string) ResponseWrapper {
buf := bytes.NewBufferString(params)
r.method = "POST"
r.body = buf
if r.header == nil {
r.header = make(map[string]string)
}
r.header["Content-type"] = "application/x-www-form-urlencoded"
return request(r)
}
type ResponseWrapper struct {
StatusCode int
Body string
Header http.Header
}
func (r *ResponseWrapper) IsSuccess() bool {
return r.StatusCode == 200
}
func (r *ResponseWrapper) ToObj(obj interface{}) {
if !r.IsSuccess() {
return
}
_ = json.Unmarshal([]byte(r.Body), &obj)
}
func (r *ResponseWrapper) ToMap() map[string]interface{} {
if !r.IsSuccess() {
return nil
}
var res map[string]interface{}
err := json.Unmarshal([]byte(r.Body), &res)
if err != nil {
return nil
}
return res
}
func request(rw *RequestWrapper) ResponseWrapper {
wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)}
client := &http.Client{}
timeout := rw.timeout
if timeout > 0 {
client.Timeout = time.Duration(timeout) * time.Second
} else {
timeout = DefTimeout
}
req, err := http.NewRequest(rw.method, rw.url, rw.body)
if err != nil {
return createRequestError(err)
}
setRequestHeader(req, rw.header)
resp, err := client.Do(req)
if err != nil {
wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error())
return wrapper
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())
return wrapper
}
wrapper.StatusCode = resp.StatusCode
wrapper.Body = string(body)
wrapper.Header = resp.Header
return wrapper
}
func setRequestHeader(req *http.Request, header map[string]string) {
req.Header.Set("User-Agent", "golang/mayflyjob")
for k, v := range header {
req.Header.Set(k, v)
}
}
func createRequestError(err error) ResponseWrapper {
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
return ResponseWrapper{0, errorMessage, make(http.Header)}
}

216
base/model.go Normal file
View File

@@ -0,0 +1,216 @@
package base
import (
"errors"
"github.com/astaxie/beego/orm"
"github.com/siddontang/go/log"
"mayfly-go/base/utils"
"reflect"
"strconv"
"strings"
"time"
)
type Model struct {
Id uint64 `orm:"column(id);auto" json:"id"`
CreateTime time.Time `orm:"column(create_time);type(datetime);null" json:"createTime"`
CreatorId uint64 `orm:"column(creator_id)" json:"creatorId"`
Creator string `orm:"column(creator)" json:"creator"`
UpdateTime time.Time `orm:"column(update_time);type(datetime);null" json:"updateTime"`
ModifierId uint64 `orm:"column(modifier_id)" json:"modifierId"`
Modifier string `orm:"column(modifier)" json:"modifier"`
}
// 获取orm querySeter
func QuerySetter(table interface{}) orm.QuerySeter {
return getOrm().QueryTable(table)
}
// 获取分页结果
func GetPage(seter orm.QuerySeter, pageParam *PageParam, models interface{}, toModels interface{}) PageResult {
count, _ := seter.Count()
if count == 0 {
return PageResult{Total: 0, List: nil}
}
_, qerr := seter.Limit(pageParam.PageSize, pageParam.PageNum-1).All(models, getFieldNames(toModels)...)
BizErrIsNil(qerr, "查询错误")
err := utils.Copy(toModels, models)
BizErrIsNil(err, "实体转换错误")
return PageResult{Total: count, List: toModels}
}
// 根据sql获取分页对象
func GetPageBySql(sql string, toModel interface{}, param *PageParam, args ...interface{}) PageResult {
selectIndex := strings.Index(sql, "SELECT ") + 7
fromIndex := strings.Index(sql, " FROM")
selectCol := sql[selectIndex:fromIndex]
countSql := strings.Replace(sql, selectCol, "COUNT(*) AS total ", 1)
// 查询count
o := getOrm()
type TotalRes struct {
Total int64
}
var totalRes TotalRes
_ = o.Raw(countSql, args).QueryRow(&totalRes)
total := totalRes.Total
if total == 0 {
return PageResult{Total: 0, List: nil}
}
// 分页查询
limitSql := sql + " LIMIT " + strconv.Itoa(param.PageNum-1) + ", " + strconv.Itoa(param.PageSize)
var maps []orm.Params
_, err := o.Raw(limitSql, args).Values(&maps)
if err != nil {
panic(errors.New("查询错误 : " + err.Error()))
}
e := ormParams2Struct(maps, toModel)
if e != nil {
panic(e)
}
return PageResult{Total: total, List: toModel}
}
func GetListBySql(sql string, params ...interface{}) *[]orm.Params {
var maps []orm.Params
_, err := getOrm().Raw(sql, params).Values(&maps)
if err != nil {
log.Error("根据sql查询数据列表失败%s", err.Error())
}
return &maps
}
// 获取所有列表数据
func GetList(seter orm.QuerySeter, model interface{}, toModel interface{}) {
_, _ = seter.All(model, getFieldNames(toModel)...)
err := utils.Copy(toModel, model)
BizErrIsNil(err, "实体转换错误")
}
// 根据toModel结构体字段查询单条记录并将值赋值给toModel
func GetOne(seter orm.QuerySeter, model interface{}, toModel interface{}) error {
err := seter.One(model, getFieldNames(toModel)...)
if err != nil {
return err
}
cerr := utils.Copy(toModel, model)
BizErrIsNil(cerr, "实体转换错误")
return nil
}
// 根据实体以及指定字段值查询实体若字段数组为空则默认用id查
func GetBy(model interface{}, fs ...string) error {
err := getOrm().Read(model, fs...)
if err != nil {
if err == orm.ErrNoRows {
return errors.New("该数据不存在")
} else {
return errors.New("查询失败")
}
}
return nil
}
func Insert(model interface{}) error {
_, err := getOrm().Insert(model)
if err != nil {
return errors.New("数据插入失败")
}
return nil
}
func Update(model interface{}, fs ...string) error {
_, err := getOrm().Update(model, fs...)
if err != nil {
return errors.New("数据更新失败")
}
return nil
}
func Delete(model interface{}, fs ...string) error {
_, err := getOrm().Delete(model, fs...)
if err != nil {
return errors.New("数据删除失败")
}
return nil
}
func getOrm() orm.Ormer {
return orm.NewOrm()
}
// 结果模型缓存
var resultModelCache = make(map[string][]string)
// 获取实体对象的字段名
func getFieldNames(obj interface{}) []string {
objType := indirectType(reflect.TypeOf(obj))
cacheKey := objType.PkgPath() + "." + objType.Name()
cache := resultModelCache[cacheKey]
if cache != nil {
return cache
}
cache = getFieldNamesByType("", reflect.TypeOf(obj))
resultModelCache[cacheKey] = cache
return cache
}
func indirectType(reflectType reflect.Type) reflect.Type {
for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
reflectType = reflectType.Elem()
}
return reflectType
}
func getFieldNamesByType(namePrefix string, reflectType reflect.Type) []string {
var fieldNames []string
if reflectType = indirectType(reflectType); reflectType.Kind() == reflect.Struct {
for i := 0; i < reflectType.NumField(); i++ {
t := reflectType.Field(i)
tName := t.Name
// 判断结构体字段是否为结构体,是的话则跳过
it := indirectType(t.Type)
if it.Kind() == reflect.Struct {
itName := it.Name()
// 如果包含Time或time则表示为time类型无需递归该结构体字段
if !strings.Contains(itName, "BaseModel") && !strings.Contains(itName, "Time") &&
!strings.Contains(itName, "time") {
fieldNames = append(fieldNames, getFieldNamesByType(tName+"__", it)...)
continue
}
}
if t.Anonymous {
fieldNames = append(fieldNames, getFieldNamesByType("", t.Type)...)
} else {
fieldNames = append(fieldNames, namePrefix+tName)
}
}
}
return fieldNames
}
func ormParams2Struct(maps []orm.Params, structs interface{}) error {
structsV := reflect.Indirect(reflect.ValueOf(structs))
valType := structsV.Type()
valElemType := valType.Elem()
sliceType := reflect.SliceOf(valElemType)
length := len(maps)
valSlice := structsV
if valSlice.IsNil() {
// Make a new slice to hold our result, same size as the original data.
valSlice = reflect.MakeSlice(sliceType, length, length)
}
for i := 0; i < length; i++ {
err := utils.Map2Struct(maps[i], valSlice.Index(i).Addr().Interface())
if err != nil {
return err
}
}
structsV.Set(valSlice)
return nil
}

71
base/model_test.go Normal file
View File

@@ -0,0 +1,71 @@
package base
import (
"fmt"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql"
"mayfly-go/base/utils"
"mayfly-go/controllers/vo"
"mayfly-go/models"
"strings"
"testing"
)
type AccountDetailVO struct {
Id int64
Username string
}
func init() {
orm.RegisterDriver("mysql", orm.DRMySQL)
orm.RegisterDataBase("default", "mysql", "root:111049@tcp(localhost:3306)/mayfly-go?charset=utf8")
orm.Debug = true
}
func TestGetList(t *testing.T) {
query := QuerySetter(new(models.Account)).OrderBy("-Id")
list := new([]AccountDetailVO)
GetList(query, new([]models.Account), list)
fmt.Println(list)
}
func TestGetOne(t *testing.T) {
model := new(models.Account)
query := QuerySetter(model).Filter("Id", 2)
adv := new(AccountDetailVO)
GetOne(query, model, adv)
fmt.Println(adv)
}
func TestMap(t *testing.T) {
//o := getOrm()
//
////v := new([]Account)
//var maps []orm.Params
//_, err := o.Raw("SELECT a.Id, a.Username, r.Id AS 'Role.Id', r.Name AS 'Role.Name' FROM " +
// "t_account a JOIN t_role r ON a.id = r.account_id").Values(&maps)
//fmt.Println(err)
//////res := new([]Account)
////model := &Account{}
////o.QueryTable("t_account").Filter("id", 1).RelatedSel().One(model)
////o.LoadRelated(model, "Role")
res := new([]vo.AccountVO)
sql := "SELECT a.Id, a.Username, r.Id AS 'Role.Id', r.Name AS 'Role.Name' FROM t_account a JOIN t_role r ON a.id = r.account_id"
//limitSql := sql + " LIMIT 1, 3"
//selectIndex := strings.Index(sql, "SELECT ") + 7
//fromIndex := strings.Index(sql, " FROM")
//selectCol := sql[selectIndex:fromIndex]
//countSql := strings.Replace(sql, selectCol, "COUNT(*)", 1)
//fmt.Println(limitSql)
//fmt.Println(selectCol)
//fmt.Println(countSql)
page := GetPageBySql(sql, res, &PageParam{PageNum: 1, PageSize: 1})
fmt.Println(page)
//return res
}
func TestCase2Camel(t *testing.T) {
fmt.Println(utils.Case2Camel("create_time"))
fmt.Println(strings.Title("username"))
}

13
base/page.go Normal file
View File

@@ -0,0 +1,13 @@
package base
// 分页参数
type PageParam struct {
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
}
// 分页结果
type PageResult struct {
Total int64 `json:"total"`
List interface{} `json:"list"`
}

66
base/result.go Normal file
View File

@@ -0,0 +1,66 @@
package base
import (
"encoding/json"
"fmt"
)
const (
SuccessCode = 200
SuccessMsg = "success"
BizErrorCode = 400
BizErrorMsg = "error"
ServerErrorCode = 500
ServerErrorMsg = "server error"
TokenErrorCode = 501
TokenErrorMsg = "token error"
)
// 统一返回结果结构体
type Result struct {
Code int16 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 将Result转为json字符串
func (r *Result) ToJson() string {
jsonData, err := json.Marshal(r)
if err != nil {
fmt.Println("data转json错误")
}
return string(jsonData)
}
// 判断该Result是否为成功状态
func (r *Result) IsSuccess() bool {
return r.Code == SuccessCode
}
// 返回成功状态的Result
// @param data 成功附带的数据消息
func Success(data interface{}) *Result {
return &Result{Code: SuccessCode, Msg: SuccessMsg, Data: data}
}
// 返回成功状态的Result
// @param data 成功不附带数据
func SuccessNoData() *Result {
return &Result{Code: SuccessCode, Msg: SuccessMsg}
}
// 返回服务器错误Result
func ServerError() *Result {
return &Result{Code: ServerErrorCode, Msg: ServerErrorMsg}
}
func Error(code int16, msg string) *Result {
return &Result{Code: code, Msg: msg}
}
func TokenError() *Result {
return &Result{Code: TokenErrorCode, Msg: TokenErrorMsg}
}

49
base/token.go Normal file
View File

@@ -0,0 +1,49 @@
package base
import (
"errors"
"github.com/dgrijalva/jwt-go"
"time"
)
const (
JwtKey = "mykey"
ExpTime = time.Hour * 24 * 7
)
type LoginAccount struct {
Id uint64
Username string
}
// 创建用户token
func CreateToken(userId uint64, username string) string {
// 带权限创建令牌
// 设置有效期过期需要重新登录获取token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": userId,
"username": username,
"exp": time.Now().Add(ExpTime).Unix(),
})
// 使用自定义字符串加密 and get the complete encoded token as a string
tokenString, err := token.SignedString([]byte(JwtKey))
BizErrIsNil(err, "token创建失败")
return tokenString
}
// 解析token并返回登录者账号信息
func ParseToken(tokenStr string) (*LoginAccount, error) {
if tokenStr == "" {
return nil, errors.New("token error")
}
// Parse token
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return []byte(JwtKey), nil
})
if err != nil || token == nil {
return nil, err
}
i := token.Claims.(jwt.MapClaims)
return &LoginAccount{Id: uint64(i["id"].(float64)), Username: i["username"].(string)}, nil
}

23
base/utils/map_utils.go Normal file
View File

@@ -0,0 +1,23 @@
package utils
import (
"reflect"
"strconv"
)
func GetString4Map(m map[string]interface{}, key string) string {
return m[key].(string)
}
func GetInt4Map(m map[string]interface{}, key string) int {
i := m[key]
iKind := reflect.TypeOf(i).Kind()
if iKind == reflect.Int {
return i.(int)
}
if iKind == reflect.String {
i, _ := strconv.Atoi(i.(string))
return i
}
return 0
}

89
base/utils/str_utils.go Normal file
View File

@@ -0,0 +1,89 @@
package utils
import (
"bytes"
"strings"
"text/template"
)
// 可判断中文
func StrLen(str string) int {
return len([]rune(str))
}
// 去除字符串左右空字符
func StrTrim(str string) string {
return strings.Trim(str, " ")
}
func SubString(str string, begin, end int) (substr string) {
// 将字符串的转换成[]rune
rs := []rune(str)
lth := len(rs)
// 简单的越界判断
if begin < 0 {
begin = 0
}
if begin >= lth {
begin = lth
}
if end > lth {
end = lth
}
// 返回子串
return string(rs[begin:end])
}
func UnicodeIndex(str, substr string) int {
// 子串在字符串的字节位置
result := strings.Index(str, substr)
if result >= 0 {
// 获得子串之前的字符串并转换成[]byte
prefix := []byte(str)[0:result]
// 将子串之前的字符串转换成[]rune
rs := []rune(string(prefix))
// 获得子串之前的字符串的长度,便是子串在字符串的字符位置
result = len(rs)
}
return result
}
// 字符串模板解析
func TemplateResolve(temp string, data interface{}) string {
t, _ := template.New("string-temp").Parse(temp)
var tmplBytes bytes.Buffer
err := t.Execute(&tmplBytes, data)
if err != nil {
panic(err)
}
return tmplBytes.String()
}
func ReverStrTemplate(temp, str string, res map[string]interface{}) {
index := UnicodeIndex(temp, "{")
ei := UnicodeIndex(temp, "}") + 1
next := StrTrim(temp[ei:])
nextContain := UnicodeIndex(next, "{")
nextIndexValue := next
if nextContain != -1 {
nextIndexValue = SubString(next, 0, nextContain)
}
key := temp[index+1 : ei-1]
// 如果后面没有内容了,则取字符串的长度即可
var valueLastIndex int
if nextIndexValue == "" {
valueLastIndex = StrLen(str)
} else {
valueLastIndex = UnicodeIndex(str, nextIndexValue)
}
value := StrTrim(SubString(str, index, valueLastIndex))
res[key] = value
// 如果后面的还有需要解析的,则递归调用解析
if nextContain != -1 {
ReverStrTemplate(next, StrTrim(SubString(str, UnicodeIndex(str, value)+StrLen(value), StrLen(str))), res)
}
}

629
base/utils/struct_utils.go Normal file
View File

@@ -0,0 +1,629 @@
package utils
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
// Copy copy things引用至copier
func Copy(toValue interface{}, fromValue interface{}) (err error) {
var (
isSlice bool
amount = 1
from = Indirect(reflect.ValueOf(fromValue))
to = Indirect(reflect.ValueOf(toValue))
)
if !to.CanAddr() {
return errors.New("copy to value is unaddressable")
}
// Return is from value is invalid
if !from.IsValid() {
return
}
fromType := IndirectType(from.Type())
toType := IndirectType(to.Type())
// Just set it if possible to assign
// And need to do copy anyway if the type is struct
if fromType.Kind() != reflect.Struct && from.Type().AssignableTo(to.Type()) {
to.Set(from)
return
}
if fromType.Kind() != reflect.Struct || toType.Kind() != reflect.Struct {
return
}
if to.Kind() == reflect.Slice {
isSlice = true
if from.Kind() == reflect.Slice {
amount = from.Len()
}
}
for i := 0; i < amount; i++ {
var dest, source reflect.Value
if isSlice {
// source
if from.Kind() == reflect.Slice {
source = Indirect(from.Index(i))
} else {
source = Indirect(from)
}
// dest
dest = Indirect(reflect.New(toType).Elem())
} else {
source = Indirect(from)
dest = Indirect(to)
}
// check source
if source.IsValid() {
fromTypeFields := deepFields(fromType)
//fmt.Printf("%#v", fromTypeFields)
// Copy from field to field or method
for _, field := range fromTypeFields {
name := field.Name
if fromField := source.FieldByName(name); fromField.IsValid() {
// has field
if toField := dest.FieldByName(name); toField.IsValid() {
if toField.CanSet() {
if !set(toField, fromField) {
if err := Copy(toField.Addr().Interface(), fromField.Interface()); err != nil {
return err
}
}
}
} else {
// try to set to method
var toMethod reflect.Value
if dest.CanAddr() {
toMethod = dest.Addr().MethodByName(name)
} else {
toMethod = dest.MethodByName(name)
}
if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
toMethod.Call([]reflect.Value{fromField})
}
}
}
}
// Copy from method to field
for _, field := range deepFields(toType) {
name := field.Name
var fromMethod reflect.Value
if source.CanAddr() {
fromMethod = source.Addr().MethodByName(name)
} else {
fromMethod = source.MethodByName(name)
}
if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 {
if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() {
values := fromMethod.Call([]reflect.Value{})
if len(values) >= 1 {
set(toField, values[0])
}
}
}
}
}
if isSlice {
if dest.Addr().Type().AssignableTo(to.Type().Elem()) {
to.Set(reflect.Append(to, dest.Addr()))
} else if dest.Type().AssignableTo(to.Type().Elem()) {
to.Set(reflect.Append(to, dest))
}
}
}
return
}
func deepFields(reflectType reflect.Type) []reflect.StructField {
var fields []reflect.StructField
if reflectType = IndirectType(reflectType); reflectType.Kind() == reflect.Struct {
for i := 0; i < reflectType.NumField(); i++ {
v := reflectType.Field(i)
if v.Anonymous {
fields = append(fields, deepFields(v.Type)...)
} else {
fields = append(fields, v)
}
}
}
return fields
}
func Indirect(reflectValue reflect.Value) reflect.Value {
for reflectValue.Kind() == reflect.Ptr {
reflectValue = reflectValue.Elem()
}
return reflectValue
}
func IndirectType(reflectType reflect.Type) reflect.Type {
for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
reflectType = reflectType.Elem()
}
return reflectType
}
func set(to, from reflect.Value) bool {
if from.IsValid() {
if to.Kind() == reflect.Ptr {
//set `to` to nil if from is nil
if from.Kind() == reflect.Ptr && from.IsNil() {
to.Set(reflect.Zero(to.Type()))
return true
} else if to.IsNil() {
to.Set(reflect.New(to.Type().Elem()))
}
to = to.Elem()
}
if from.Type().ConvertibleTo(to.Type()) {
to.Set(from.Convert(to.Type()))
} else if scanner, ok := to.Addr().Interface().(sql.Scanner); ok {
err := scanner.Scan(from.Interface())
if err != nil {
return false
}
} else if from.Kind() == reflect.Ptr {
return set(to, from.Elem())
} else {
return false
}
}
return true
}
func Map2Struct(m map[string]interface{}, s interface{}) error {
toValue := Indirect(reflect.ValueOf(s))
if !toValue.CanAddr() {
return errors.New("to value is unaddressable")
}
innerStructMaps := getInnerStructMaps(m)
if len(innerStructMaps) != 0 {
for k, v := range innerStructMaps {
var fieldV reflect.Value
if strings.Contains(k, ".") {
fieldV = getFiledValueByPath(k, toValue)
} else {
fieldV = toValue.FieldByName(k)
}
if !fieldV.CanSet() || !fieldV.CanAddr() {
continue
}
fieldT := fieldV.Type().Elem()
if fieldT.Kind() != reflect.Struct {
return errors.New(k + "不是结构体")
}
// 如果值为nil则默认创建一个并赋值
if fieldV.IsNil() {
fieldV.Set(reflect.New(fieldT))
}
err := Map2Struct(v, fieldV.Addr().Interface())
if err != nil {
return err
}
}
}
var err error
for k, v := range m {
if v == nil {
continue
}
k = strings.Title(k)
// 如果key含有下划线则将其转为驼峰
if strings.Contains(k, "_") {
k = Case2Camel(k)
}
fieldV := toValue.FieldByName(k)
if !fieldV.CanSet() {
continue
}
err = decode(k, v, fieldV)
if err != nil {
return err
}
}
return nil
}
func Maps2Structs(maps []map[string]interface{}, structs interface{}) error {
structsV := reflect.Indirect(reflect.ValueOf(structs))
valType := structsV.Type()
valElemType := valType.Elem()
sliceType := reflect.SliceOf(valElemType)
length := len(maps)
valSlice := structsV
if valSlice.IsNil() {
// Make a new slice to hold our result, same size as the original data.
valSlice = reflect.MakeSlice(sliceType, length, length)
}
for i := 0; i < length; i++ {
err := Map2Struct(maps[i], valSlice.Index(i).Addr().Interface())
if err != nil {
return err
}
}
structsV.Set(valSlice)
return nil
}
func getFiledValueByPath(path string, value reflect.Value) reflect.Value {
split := strings.Split(path, ".")
for _, v := range split {
if value.Type().Kind() == reflect.Ptr {
// 如果值为nil则创建并赋值
if value.IsNil() {
value.Set(reflect.New(IndirectType(value.Type())))
}
value = value.Elem()
}
value = value.FieldByName(v)
}
return value
}
func getInnerStructMaps(m map[string]interface{}) map[string]map[string]interface{} {
key2map := make(map[string]map[string]interface{})
for k, v := range m {
if !strings.Contains(k, ".") {
continue
}
lastIndex := strings.LastIndex(k, ".")
prefix := k[0:lastIndex]
m2 := key2map[prefix]
if m2 == nil {
key2map[prefix] = map[string]interface{}{k[lastIndex+1:]: v}
} else {
m2[k[lastIndex+1:]] = v
}
delete(m, k)
}
return key2map
}
// decode等方法摘抄自mapstructure库
func decode(name string, input interface{}, outVal reflect.Value) error {
var inputVal reflect.Value
if input != nil {
inputVal = reflect.ValueOf(input)
// We need to check here if input is a typed nil. Typed nils won't
// match the "input == nil" below so we check that here.
if inputVal.Kind() == reflect.Ptr && inputVal.IsNil() {
input = nil
}
}
if !inputVal.IsValid() {
// If the input value is invalid, then we just set the value
// to be the zero value.
outVal.Set(reflect.Zero(outVal.Type()))
return nil
}
var err error
outputKind := getKind(outVal)
switch outputKind {
case reflect.Int:
err = decodeInt(name, input, outVal)
case reflect.Uint:
err = decodeUint(name, input, outVal)
case reflect.Float32:
err = decodeFloat(name, input, outVal)
case reflect.String:
err = decodeString(name, input, outVal)
case reflect.Ptr:
_, err = decodePtr(name, input, outVal)
default:
// If we reached this point then we weren't able to decode it
return fmt.Errorf("%s: unsupported type: %s", name, outputKind)
}
return err
}
func decodeInt(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
dataKind := getKind(dataVal)
dataType := dataVal.Type()
switch {
case dataKind == reflect.Int:
val.SetInt(dataVal.Int())
case dataKind == reflect.Uint:
val.SetInt(int64(dataVal.Uint()))
case dataKind == reflect.Float32:
val.SetInt(int64(dataVal.Float()))
case dataKind == reflect.Bool:
if dataVal.Bool() {
val.SetInt(1)
} else {
val.SetInt(0)
}
case dataKind == reflect.String:
i, err := strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
if err == nil {
val.SetInt(i)
} else {
return fmt.Errorf("cannot parse '%s' as int: %s", name, err)
}
case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number":
jn := data.(json.Number)
i, err := jn.Int64()
if err != nil {
return fmt.Errorf(
"error decoding json.Number into %s: %s", name, err)
}
val.SetInt(i)
default:
return fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type())
}
return nil
}
func decodeUint(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
dataKind := getKind(dataVal)
dataType := dataVal.Type()
switch {
case dataKind == reflect.Int:
i := dataVal.Int()
if i < 0 {
return fmt.Errorf("cannot parse '%s', %d overflows uint",
name, i)
}
val.SetUint(uint64(i))
case dataKind == reflect.Uint:
val.SetUint(dataVal.Uint())
case dataKind == reflect.Float32:
f := dataVal.Float()
if f < 0 {
return fmt.Errorf("cannot parse '%s', %f overflows uint",
name, f)
}
val.SetUint(uint64(f))
case dataKind == reflect.Bool:
if dataVal.Bool() {
val.SetUint(1)
} else {
val.SetUint(0)
}
case dataKind == reflect.String:
i, err := strconv.ParseUint(dataVal.String(), 0, val.Type().Bits())
if err == nil {
val.SetUint(i)
} else {
return fmt.Errorf("cannot parse '%s' as uint: %s", name, err)
}
case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number":
jn := data.(json.Number)
i, err := jn.Int64()
if err != nil {
return fmt.Errorf(
"error decoding json.Number into %s: %s", name, err)
}
if i < 0 {
return fmt.Errorf("cannot parse '%s', %d overflows uint",
name, i)
}
val.SetUint(uint64(i))
default:
return fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type())
}
return nil
}
func decodeFloat(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
dataKind := getKind(dataVal)
dataType := dataVal.Type()
switch {
case dataKind == reflect.Int:
val.SetFloat(float64(dataVal.Int()))
case dataKind == reflect.Uint:
val.SetFloat(float64(dataVal.Uint()))
case dataKind == reflect.Float32:
val.SetFloat(dataVal.Float())
case dataKind == reflect.Bool:
if dataVal.Bool() {
val.SetFloat(1)
} else {
val.SetFloat(0)
}
case dataKind == reflect.String:
f, err := strconv.ParseFloat(dataVal.String(), val.Type().Bits())
if err == nil {
val.SetFloat(f)
} else {
return fmt.Errorf("cannot parse '%s' as float: %s", name, err)
}
case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number":
jn := data.(json.Number)
i, err := jn.Float64()
if err != nil {
return fmt.Errorf(
"error decoding json.Number into %s: %s", name, err)
}
val.SetFloat(i)
default:
return fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type())
}
return nil
}
func decodeString(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
dataKind := getKind(dataVal)
converted := true
switch {
case dataKind == reflect.String:
val.SetString(dataVal.String())
case dataKind == reflect.Bool:
if dataVal.Bool() {
val.SetString("1")
} else {
val.SetString("0")
}
case dataKind == reflect.Int:
val.SetString(strconv.FormatInt(dataVal.Int(), 10))
case dataKind == reflect.Uint:
val.SetString(strconv.FormatUint(dataVal.Uint(), 10))
case dataKind == reflect.Float32:
val.SetString(strconv.FormatFloat(dataVal.Float(), 'f', -1, 64))
case dataKind == reflect.Slice,
dataKind == reflect.Array:
dataType := dataVal.Type()
elemKind := dataType.Elem().Kind()
switch elemKind {
case reflect.Uint8:
var uints []uint8
if dataKind == reflect.Array {
uints = make([]uint8, dataVal.Len(), dataVal.Len())
for i := range uints {
uints[i] = dataVal.Index(i).Interface().(uint8)
}
} else {
uints = dataVal.Interface().([]uint8)
}
val.SetString(string(uints))
default:
converted = false
}
default:
converted = false
}
if !converted {
return fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type())
}
return nil
}
func decodePtr(name string, data interface{}, val reflect.Value) (bool, error) {
// If the input data is nil, then we want to just set the output
// pointer to be nil as well.
isNil := data == nil
if !isNil {
switch v := reflect.Indirect(reflect.ValueOf(data)); v.Kind() {
case reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Ptr,
reflect.Slice:
isNil = v.IsNil()
}
}
if isNil {
if !val.IsNil() && val.CanSet() {
nilValue := reflect.New(val.Type()).Elem()
val.Set(nilValue)
}
return true, nil
}
// Create an element of the concrete (non pointer) type and decode
// into that. Then set the value of the pointer to this type.
valType := val.Type()
valElemType := valType.Elem()
if val.CanSet() {
realVal := val
if realVal.IsNil() {
realVal = reflect.New(valElemType)
}
if err := decode(name, data, reflect.Indirect(realVal)); err != nil {
return false, err
}
val.Set(realVal)
} else {
if err := decode(name, data, reflect.Indirect(val)); err != nil {
return false, err
}
}
return false, nil
}
func getKind(val reflect.Value) reflect.Kind {
kind := val.Kind()
switch {
case kind >= reflect.Int && kind <= reflect.Int64:
return reflect.Int
case kind >= reflect.Uint && kind <= reflect.Uint64:
return reflect.Uint
case kind >= reflect.Float32 && kind <= reflect.Float64:
return reflect.Float32
default:
return kind
}
}
// 下划线写法转为驼峰写法
func Case2Camel(name string) string {
name = strings.Replace(name, "_", " ", -1)
name = strings.Title(name)
return strings.Replace(name, " ", "", -1)
}
func isBlank(value reflect.Value) bool {
switch value.Kind() {
case reflect.String:
return value.Len() == 0
case reflect.Bool:
return !value.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return value.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return value.Uint() == 0
case reflect.Float32, reflect.Float64:
return value.Float() == 0
case reflect.Interface, reflect.Ptr:
return value.IsNil()
}
return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}

View File

@@ -0,0 +1,195 @@
package utils
import (
"fmt"
"github.com/mitchellh/mapstructure"
"reflect"
"strings"
"testing"
"time"
)
type Src struct {
Id *int64 `json:"id"`
Username string `json:"username"`
CreateTime time.Time `json:"time"`
UpdateTime time.Time
Inner *SrcInner
}
type SrcInner struct {
Name string
Desc string
Id int64
Dest *Dest
}
type Dest struct {
Username string
Id int64
CreateTime time.Time
Inner *DestInner
}
type DestInner struct {
Desc string
}
func TestDeepFields(t *testing.T) {
////src := Src{Username: "test", Id: 1000, CreateTime: time.Now()}
//si := SrcInner{Desc: "desc"}
//src.Inner = &si
////src.Id = 1222
//dest := new(Dest)
//err := structutils.Copy(dest, src)
//if err != nil {
// fmt.Println(err.Error())
//} else {
// fmt.Println(dest)
//}
}
func TestGetFieldNames(t *testing.T) {
//names := structutils.GetFieldNames(new(Src))
//fmt.Println(names)
}
func TestMaps2Structs(t *testing.T) {
mapInstance := make(map[string]interface{})
mapInstance["Username"] = "liang637210"
mapInstance["Id"] = 28
mapInstance["CreateTime"] = time.Now()
mapInstance["Creator"] = "createor"
mapInstance["Inner.Id"] = 10
mapInstance["Inner.Name"] = "hahah"
mapInstance["Inner.Desc"] = "inner desc"
mapInstance["Inner.Dest.Username"] = "inner dest uername"
mapInstance["Inner.Dest.Inner.Desc"] = "inner dest inner desc"
mapInstance2 := make(map[string]interface{})
mapInstance2["Username"] = "liang6372102"
mapInstance2["Id"] = 282
mapInstance2["CreateTime"] = time.Now()
mapInstance2["Creator"] = "createor2"
mapInstance2["Inner.Id"] = 102
mapInstance2["Inner.Name"] = "hahah2"
mapInstance2["Inner.Desc"] = "inner desc2"
mapInstance2["Inner.Dest.Username"] = "inner dest uername2"
mapInstance2["Inner.Dest.Inner.Desc"] = "inner dest inner desc2"
maps := make([]map[string]interface{}, 2)
maps[0] = mapInstance
maps[1] = mapInstance2
res := new([]Src)
err := Maps2Structs(maps, res)
if err != nil {
fmt.Println(err)
}
}
func TestMap2Struct(t *testing.T) {
mapInstance := make(map[string]interface{})
mapInstance["Username"] = "liang637210"
mapInstance["Id"] = 12
mapInstance["CreateTime"] = time.Now()
mapInstance["Creator"] = "createor"
mapInstance["Inner.Id"] = nil
mapInstance["Inner.Name"] = "hahah"
mapInstance["Inner.Desc"] = "inner desc"
mapInstance["Inner.Dest.Username"] = "inner dest uername"
mapInstance["Inner.Dest.Inner.Desc"] = "inner dest inner desc"
//innerMap := make(map[string]interface{})
//innerMap["Name"] = "Innername"
//a := new(Src)
////a.Inner = new(SrcInner)
//
//stime := time.Now().UnixNano()
//for i := 0; i < 1000000; i++ {
// err := structutils.Map2Struct(mapInstance, a)
// if err != nil {
// fmt.Println(err)
// }
//}
//etime := time.Now().UnixNano()
//fmt.Println(etime - stime)
//if err != nil {
// fmt.Println(err)
//} else {
// fmt.Println(a)
//}
s := new(Src)
//name, b := structutils.IndirectType(reflect.TypeOf(s)).FieldByName("Inner")
//if structutils.IndirectType(name.Type).Kind() != reflect.Struct {
// fmt.Println(name.Name + "不是结构体")
//} else {
// //innerType := name.Type
// innerValue := structutils.Indirect(reflect.ValueOf(s)).FieldByName("Inner")
// //if innerValue.IsValid() && innerValue.IsNil() {
// // innerValue.Set(reflect.New(innerValue.Type().Elem()))
// //}
// if !innerValue.IsValid() {
// fmt.Println("is valid")
// } else {
// //innerValue.Set(reflect.New(innerValue.Type()))
// fmt.Println(innerValue.CanSet())
// fmt.Println(innerValue.CanAddr())
// //mapstructure.Decode(innerMap, innerValue.Addr().Interface())
// }
//
//}
//
//fmt.Println(name, b)
//将 map 转换为指定的结构体
if err := mapstructure.Decode(mapInstance, &s); err != nil {
fmt.Println(err)
}
fmt.Printf("map2struct后得到的 struct 内容为:%v", s)
}
func getPrefixKeyMap(m map[string]interface{}) map[string]map[string]interface{} {
key2map := make(map[string]map[string]interface{})
for k, v := range m {
if !strings.Contains(k, ".") {
continue
}
lastIndex := strings.LastIndex(k, ".")
prefix := k[0:lastIndex]
m2 := key2map[prefix]
if m2 == nil {
key2map[prefix] = map[string]interface{}{k[lastIndex+1:]: v}
} else {
m2[k[lastIndex+1:]] = v
}
delete(m, k)
}
return key2map
}
func TestReflect(t *testing.T) {
type dog struct {
LegCount int
}
// 获取dog实例的反射值对象
valueOfDog := reflect.ValueOf(&dog{}).Elem()
// 获取legCount字段的值
vLegCount := valueOfDog.FieldByName("LegCount")
fmt.Println(vLegCount.CanSet())
fmt.Println(vLegCount.CanAddr())
// 尝试设置legCount的值(这里会发生崩溃)
vLegCount.SetInt(4)
}
func TestTemplateResolve(t *testing.T) {
d := make(map[string]string)
d["Name"] = "黄先生"
d["Age"] = "23jlfdsjf"
resolve := TemplateResolve("{{.Name}} is name, and {{.Age}} is age", d)
fmt.Println(resolve)
}

21
conf/app.conf Normal file
View File

@@ -0,0 +1,21 @@
appname = mayfly-job
httpport = 8888
copyrequestbody = true
autorender = false
EnableErrorsRender = false
runmode = "dev"
; mysqluser = "root"
; mysqlpass = "111049"
; mysqlurls = "127.0.0.1"
; mysqldb = "mayfly-job"
EnableAdmin = true
AdminHttpAddr = 0.0.0.0 #默认监听地址是localhost
AdminHttpPort = 8088
[dev]
httpport = 8888
[prod]
httpport = 8080
[test]
httpport = 8888

42
controllers/account.go Normal file
View File

@@ -0,0 +1,42 @@
package controllers
import (
"mayfly-go/base"
"mayfly-go/controllers/form"
"mayfly-go/models"
)
type AccountController struct {
base.Controller
}
//func (c *AccountController) URLMapping() {
// c.Mapping("Login", c.Login)
// c.Mapping("Accounts", c.Accounts)
//}
// @router /accounts/login [post]
func (c *AccountController) Login() {
c.ReturnData(false, func(la *base.LoginAccount) interface{} {
loginForm := &form.LoginForm{}
c.UnmarshalBodyAndValid(loginForm)
a := &models.Account{Username: loginForm.Username, Password: loginForm.Password}
base.BizErrIsNil(base.GetBy(a, "Username", "Password"), "用户名或密码错误")
return map[string]interface{}{
"token": base.CreateToken(a.Id, a.Username),
"username": a.Username,
}
})
}
// @router /accounts [get]
func (c *AccountController) Accounts() {
c.ReturnData(true, func(account *base.LoginAccount) interface{} {
//s := c.GetString("username")
//query := models.QuerySetter(new(models.Account)).OrderBy("-Id").RelatedSel()
//return models.GetPage(query, c.GetPageParam(), new([]models.Account), new([]vo.AccountVO))
return models.ListAccount(c.GetPageParam())
})
}

12
controllers/form/form.go Normal file
View File

@@ -0,0 +1,12 @@
package form
// 登录表单
type LoginForm struct {
Username string `valid:"Required"`
Password string `valid:"Required"`
}
type MachineRunForm struct {
MachineId int64 `valid:"Required"`
Cmd string `valid:"Required"`
}

90
controllers/machine.go Normal file
View File

@@ -0,0 +1,90 @@
package controllers
import (
"github.com/gorilla/websocket"
"mayfly-go/base"
"mayfly-go/machine"
"mayfly-go/models"
"net/http"
"strconv"
)
type MachineController struct {
base.Controller
}
var upGrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024 * 1024 * 10,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func (c *MachineController) Machines() {
c.ReturnData(true, func(account *base.LoginAccount) interface{} {
return models.GetMachineList(c.GetPageParam())
})
}
func (c *MachineController) Run() {
c.ReturnData(true, func(account *base.LoginAccount) interface{} {
cmd := c.GetString("cmd")
base.NotEmpty(cmd, "cmd不能为空")
return machine.GetCli(c.GetMachineId()).Run(cmd)
})
}
// 系统基本信息
func (c *MachineController) SysInfo() {
c.ReturnData(true, func(account *base.LoginAccount) interface{} {
return machine.GetSystemInfo(machine.GetCli(c.GetMachineId()))
})
}
// top命令信息
func (c *MachineController) Top() {
c.ReturnData(true, func(account *base.LoginAccount) interface{} {
return machine.GetTop(machine.GetCli(c.GetMachineId()))
})
}
func (c *MachineController) GetProcessByName() {
c.ReturnData(true, func(account *base.LoginAccount) interface{} {
name := c.GetString("name")
base.NotEmpty(name, "name不能为空")
return machine.GetProcessByName(machine.GetCli(c.GetMachineId()), name)
})
}
//func (c *MachineController) WsSSH() {
// wsConn, err := upGrader.Upgrade(c.Ctx.ResponseWriter, c.Ctx.Request, nil)
// if err != nil {
// panic(base.NewBizErr("获取requst responsewirte错误"))
// }
//
// cols, _ := c.GetInt("col", 80)
// rows, _ := c.GetInt("rows", 40)
//
// sws, err := machine.NewLogicSshWsSession(cols, rows, true, machine.GetCli(c.GetMachineId()), wsConn)
// if sws == nil {
// panic(base.NewBizErr("连接失败"))
// }
// //if wshandleError(wsConn, err) {
// // return
// //}
// defer sws.Close()
//
// quitChan := make(chan bool, 3)
// sws.Start(quitChan)
// go sws.Wait(quitChan)
//
// <-quitChan
//}
func (c *MachineController) GetMachineId() uint64 {
machineId, _ := strconv.Atoi(c.Ctx.Input.Param(":machineId"))
base.IsTrue(machineId > 0, "machineId错误")
return uint64(machineId)
}

31
controllers/vo/vo.go Normal file
View File

@@ -0,0 +1,31 @@
package vo
import "time"
type AccountVO struct {
//models.BaseModel
Id *int64 `json:"id"`
Username *string `json:"username"`
CreateTime *string `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`
Role *RoleVO `json:"roles"`
//Status int8 `json:"status"`
}
type MachineVO struct {
//models.BaseModel
Id *int64 `json:"id"`
Name *string `json:"name"`
Username *string `json:"username"`
Ip *string `json:"ip"`
Port *int `json:"port"`
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`
}
type RoleVO struct {
Id *int64
Name *string
}

21
go.mod Normal file
View File

@@ -0,0 +1,21 @@
module mayfly-go
go 1.13
require github.com/astaxie/beego v1.12.1
require github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gliderlabs/ssh v0.2.2
github.com/go-sql-driver/mysql v1.4.1
github.com/gorilla/websocket v1.4.2
github.com/mitchellh/mapstructure v1.3.3
github.com/pkg/sftp v1.11.0
github.com/robfig/cron/v3 v3.0.1
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726
github.com/smartystreets/goconvey v1.6.4
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
google.golang.org/appengine v1.6.6 // indirect
)

95
go.sum Normal file
View File

@@ -0,0 +1,95 @@
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM=
github.com/astaxie/beego v1.12.1 h1:dfpuoxpzLVgclveAXe4PyNKqkzgm5zF4tgF2B3kkM2I=
github.com/astaxie/beego v1.12.1/go.mod h1:kPBWpSANNbSdIqOc8SUL9h+1oyBMZhROeYsXQDbidWQ=
github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg=
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20200117065230-39095c1d176c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

188
machine/machine.go Normal file
View File

@@ -0,0 +1,188 @@
package machine
import (
"errors"
"fmt"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"io"
"mayfly-go/base"
"mayfly-go/models"
"net"
"os"
"sync"
"time"
)
// 客户端信息
type Cli struct {
machine *models.Machine
// ssh客户端
client *ssh.Client
}
// 客户端缓存
var clientCache sync.Map
var mutex sync.Mutex
// 从缓存中获取客户端信息,不存在则查库,并新建
func GetCli(machineId uint64) *Cli {
mutex.Lock()
defer mutex.Unlock()
load, ok := clientCache.Load(machineId)
if ok {
return load.(*Cli)
}
cli, err := newClient(models.GetMachineById(machineId))
if err != nil {
panic(base.NewBizErr(err.Error()))
}
clientCache.LoadOrStore(machineId, cli)
return cli
}
//根据机器信息创建客户端对象
func newClient(machine *models.Machine) (*Cli, error) {
if machine == nil {
return nil, errors.New("机器不存在")
}
cli := new(Cli)
cli.machine = machine
err := cli.connect()
if err != nil {
return nil, errors.New("获取机器client失败" + err.Error())
}
return cli, nil
}
//连接
func (c *Cli) connect() error {
// 如果已经有client则直接返回
if c.client != nil {
return nil
}
m := c.machine
config := ssh.ClientConfig{
User: m.Username,
Auth: []ssh.AuthMethod{ssh.Password(m.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
sshClient, err := ssh.Dial("tcp", addr, &config)
if err != nil {
return err
}
c.client = sshClient
return nil
}
// 测试连接
func TestConn(m *models.Machine) (*ssh.Client, error) {
config := ssh.ClientConfig{
User: m.Username,
Auth: []ssh.AuthMethod{ssh.Password(m.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
sshClient, err := ssh.Dial("tcp", addr, &config)
if err != nil {
return nil, err
}
return sshClient, nil
}
// 关闭client和并从缓存中移除
func (c *Cli) Close() {
if c.client != nil {
c.client.Close()
}
if c.machine.Id > 0 {
clientCache.Delete(c.machine.Id)
}
}
// 获取sftp client
func (c *Cli) GetSftpCli() *sftp.Client {
if c.client == nil {
if err := c.connect(); err != nil {
panic(base.NewBizErr("连接ssh失败" + err.Error()))
}
}
client, serr := sftp.NewClient(c.client, sftp.MaxPacket(1<<15))
if serr != nil {
panic(base.NewBizErr("获取sftp client失败" + serr.Error()))
}
return client
}
// 获取session
func (c *Cli) GetSession() (*ssh.Session, error) {
if c.client == nil {
if err := c.connect(); err != nil {
return nil, err
}
}
return c.client.NewSession()
}
//执行shell
//@param shell shell脚本命令
func (c *Cli) Run(shell string) string {
session, err := c.GetSession()
if err != nil {
panic(base.NewBizErr("获取ssh session失败" + err.Error()))
}
defer session.Close()
buf, rerr := session.CombinedOutput(shell)
if rerr != nil {
panic(base.NewBizErr("执行命令失败:" + rerr.Error()))
}
return string(buf)
}
//执行带交互的命令
func (c *Cli) RunTerminal(shell string, stdout, stderr io.Writer) error {
session, err := c.GetSession()
if err != nil {
return err
}
//defer session.Close()
fd := int(os.Stdin.Fd())
oldState, err := terminal.MakeRaw(fd)
if err != nil {
panic(err)
}
defer terminal.Restore(fd, oldState)
session.Stdout = stdout
session.Stderr = stderr
session.Stdin = os.Stdin
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
panic(err)
}
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 1, // enable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := session.RequestPty("xterm-256color", termHeight, termWidth, modes); err != nil {
return err
}
return session.Run(shell)
}

142
machine/machine_test.go Normal file
View File

@@ -0,0 +1,142 @@
package machine
import (
"fmt"
"mayfly-go/base/utils"
"strings"
"testing"
)
func TestSSH(t *testing.T) {
//ssh.ListenAndServe("148.70.36.197")
//cli := New("148.70.36.197", "root", "gea&630_..91mn#", 22)
////output, err := cli.Run("free -h")
////fmt.Printf("%v\n%v", output, err)
//err := cli.RunTerminal("tail -f /usr/local/java/logs/eatlife-info.log", os.Stdout, os.Stdin)
//fmt.Println(err)
res := "top - 17:14:07 up 5 days, 6:30, 2 users, load average: 0.03, 0.04, 0.05\nTasks: 101 total, 1 running, 100 sleeping, 0 stopped, 0 zombie\n%Cpu(s): 6.2 us, 0.0 sy, 0.0 ni, 93.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st\nKiB Mem : 1882012 total, 73892 free, 770360 used, 1037760 buff/cache\nKiB Swap: 0 total, 0 free, 0 used. 933492 avail Mem"
split := strings.Split(res, "\n")
//var firstLine string
//for i := 0; i < len(split); i++ {
// if i == 0 {
// val := strings.Split(split[i], "top -")[1]
// vals := strings.Split(val, ",")
//
// }
//}
firstLine := strings.Split(strings.Split(split[0], "top -")[1], ",")
// 17:14:07 up 5 days
up := strings.Trim(strings.Split(firstLine[0], "up")[1], " ") + firstLine[1]
// 2 users
users := strings.Split(strings.Trim(firstLine[2], " "), " ")[0]
// load average: 0.03
oneMinLa := strings.Trim(strings.Split(strings.Trim(firstLine[3], " "), ":")[1], " ")
fiveMinLa := strings.Trim(firstLine[4], " ")
fietMinLa := strings.Trim(firstLine[5], " ")
fmt.Println(firstLine, up, users, oneMinLa, fiveMinLa, fietMinLa)
tasks := Parse(strings.Split(split[1], "Tasks:")[1])
cpu := Parse(strings.Split(split[2], "%Cpu(s):")[1])
mem := Parse(strings.Split(split[3], "KiB Mem :")[1])
fmt.Println(tasks, cpu, mem)
}
func Parse(val string) map[string]string {
res := make(map[string]string)
vals := strings.Split(val, ",")
for i := 0; i < len(vals); i++ {
trimData := strings.Trim(vals[i], " ")
keyValue := strings.Split(trimData, " ")
res[keyValue[1]] = keyValue[0]
}
return res
}
func TestTemplateRev(t *testing.T) {
temp := "hello my name is {name} hahahaha lihaiba {age} years old {public}"
str := "hello my name is hmlhmlhm 慌慌信息 hahahaha lihaiba 15 years old private protected"
//temp1 := " top - {up}, {users} users, load average: {loadavg}"
//str1 := " top - 17:14:07 up 5 days, 6:30, 2 users, load average: 0.03, 0.04, 0.05"
//taskTemp := "Tasks: {total} total, {running} running, {sleeping} sleeping, {stopped} stopped, {zombie} zombie"
//taskVal := "Tasks: 101 total, 1 running, 100 sleeping, 0 stopped, 0 zombie"
//nameRunne := []rune(str)
//index := strings.Index(temp, "{")
//ei := strings.Index(temp, "}") + 1
//next := temp[ei:]
//key := temp[index+1 : ei-1]
//value := SubString(str, index, UnicodeIndex(str, next))
res := make(map[string]interface{})
utils.ReverStrTemplate(temp, str, res)
fmt.Println(res)
}
//func ReverStrTemplate(temp, str string, res map[string]string) {
// index := UnicodeIndex(temp, "{")
// ei := UnicodeIndex(temp, "}") + 1
// next := temp[ei:]
// nextContain := UnicodeIndex(next, "{")
// nextIndexValue := next
// if nextContain != -1 {
// nextIndexValue = SubString(next, 0, nextContain)
// }
// key := temp[index+1 : ei-1]
// // 如果后面没有内容了,则取字符串的长度即可
// var valueLastIndex int
// if nextIndexValue == "" {
// valueLastIndex = StrLen(str)
// } else {
// valueLastIndex = UnicodeIndex(str, nextIndexValue)
// }
// value := SubString(str, index, valueLastIndex)
// res[key] = value
//
// if nextContain != -1 {
// ReverStrTemplate(next, SubString(str, UnicodeIndex(str, value)+StrLen(value), StrLen(str)), res)
// }
//}
//
//func StrLen(str string) int {
// return len([]rune(str))
//}
//
//func SubString(str string, begin, end int) (substr string) {
// // 将字符串的转换成[]rune
// rs := []rune(str)
// lth := len(rs)
//
// // 简单的越界判断
// if begin < 0 {
// begin = 0
// }
// if begin >= lth {
// begin = lth
// }
// if end > lth {
// end = lth
// }
//
// // 返回子串
// return string(rs[begin:end])
//}
//
//func UnicodeIndex(str, substr string) int {
// // 子串在字符串的字节位置
// result := strings.Index(str, substr)
// if result >= 0 {
// // 获得子串之前的字符串并转换成[]byte
// prefix := []byte(str)[0:result]
// // 将子串之前的字符串转换成[]rune
// rs := []rune(string(prefix))
// // 获得子串之前的字符串的长度,便是子串在字符串的字符位置
// result = len(rs)
// }
//
// return result
//}
func TestRunShellFile(t *testing.T) {
}

54
machine/shell.go Normal file
View File

@@ -0,0 +1,54 @@
package machine
import (
"github.com/siddontang/go/log"
"io/ioutil"
"mayfly-go/base"
"mayfly-go/base/utils"
"mayfly-go/models"
"time"
)
const BasePath = "./machine/shell/"
const MonitorTemp = "cpuRate:{cpuRate}%,memRate:{memRate}%,sysLoad:{sysLoad}\n"
// shell文件内容缓存避免每次读取文件
var shellCache = make(map[string]string)
func GetProcessByName(cli *Cli, name string) string {
return cli.Run(getShellContent("sys_info"))
}
func GetSystemInfo(cli *Cli) string {
return cli.Run(getShellContent("system_info"))
}
func GetMonitorInfo(cli *Cli) *models.MachineMonitor {
mm := new(models.MachineMonitor)
res := cli.Run(getShellContent("monitor"))
resMap := make(map[string]interface{})
utils.ReverStrTemplate(MonitorTemp, res, resMap)
err := utils.Map2Struct(resMap, mm)
if err != nil {
log.Error("解析machine monitor: %s", err.Error())
return nil
}
mm.MachineId = cli.machine.Id
mm.CreateTime = time.Now()
return mm
}
// 获取shell内容
func getShellContent(name string) string {
cacheShell := shellCache[name]
if cacheShell != "" {
return cacheShell
}
bytes, err := ioutil.ReadFile(BasePath + name + ".sh")
base.ErrIsNil(err, "获取shell文件失败")
shellStr := string(bytes)
shellCache[name] = shellStr
return shellStr
}

View File

@@ -0,0 +1,23 @@
#! /bin/bash
# Function: 根据输入的程序的名字过滤出所对应的PID并显示出详细信息如果有几个PID则全部显示
NAME=%s
N=`ps -aux | grep $NAME | grep -v grep | wc -l` ##统计进程总数
if [ $N -le 0 ];then
echo "该进程名没有运行!"
fi
i=1
while [ $N -gt 0 ]
do
echo "进程PID: `ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $2}'`"
echo "进程命令:`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $11}'`"
echo "进程所属用户: `ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $1}'`"
echo "CPU占用率`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $3}'`%"
echo "内存占用率:`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $4}'`%"
echo "进程开始运行的时刻:`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $9}'`"
echo "进程运行的时间:` ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $11}'`"
echo "进程状态:`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $8}'`"
echo "进程虚拟内存:`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $5}'`"
echo "进程共享内存:`ps -aux | grep $NAME | grep -v grep | awk 'NR=='$i'{print $0}'| awk '{print $6}'`"
echo "***************************************************************"
let N-- i++
done

13
machine/shell/monitor.sh Normal file
View File

@@ -0,0 +1,13 @@
# 获取监控信息
function get_monitor_info() {
cpu_rate=$(cat /proc/stat | awk '/cpu/{printf("%.2f%\n"), ($2+$4)*100/($2+$4+$5)}' | awk '{print $0}' | head -1)
mem_rate=$(free -m | sed -n '2p' | awk '{print""($3/$2)*100"%"}')
sys_load=$(uptime | cut -d: -f5)
cat <<EOF | column -t
cpuRate:${cpu_rate},memRate:${mem_rate},sysLoad:${sys_load}
EOF
}
get_monitor_info

192
machine/shell/sys_info.sh Normal file
View File

@@ -0,0 +1,192 @@
#!/bin/bash
# func:sys info check
[ $(id -u) -ne 0 ] && echo "请用root用户执行此脚本" && exit 1
sysversion=$(rpm -q centos-release | cut -d- -f3)
line="-------------------------------------------------"
# 获取系统cpu信息
function get_cpu_info() {
Physical_CPUs=$(grep "physical id" /proc/cpuinfo | sort | uniq | wc -l)
Virt_CPUs=$(grep "processor" /proc/cpuinfo | wc -l)
CPU_Kernels=$(grep "cores" /proc/cpuinfo | uniq | awk -F ': ' '{print $2}')
CPU_Type=$(grep "model name" /proc/cpuinfo | awk -F ': ' '{print $2}' | sort | uniq)
CPU_Arch=$(uname -m)
cpu_usage=$(cat /proc/stat | awk '/cpu/{printf("%.2f%\n"), ($2+$4)*100/($2+$4+$5)}' | awk '{print $0}' | head -1)
#echo -e '\033[32m CPU信息\033[0m'
echo -e ' CPU信息'
cat <<EOF | column -t
物理CPU个数: $Physical_CPUs
逻辑CPU个数: $Virt_CPUs
每CPU核心数: $CPU_Kernels
CPU型号: $CPU_Type
CPU架构: $CPU_Arch
CPU使用率: $cpu_usage
EOF
}
# 获取系统内存信息
function get_mem_info() {
Total=$(free -m | sed -n '2p' | awk '{print $2"M"}')
Used=$(free -m | sed -n '2p' | awk '{print $3"M"}')
Rate=$(free -m | sed -n '2p' | awk '{print""($3/$2)*100"%"}')
echo -e ' 内存信息:'
cat <<EOF | column -t
内存总容量:$Total
内存已使用:$Used
内存使用率:$Rate
EOF
}
# 获取系统网络信息
function get_net_info() {
pri_ipadd=$(ifconfig | awk 'NR==2{print $2}')
#pub_ipadd=$(curl ip.sb 2>&1)
pub_ipadd=$(curl -s http://ddns.oray.com/checkip | awk -F ":" '{print $2}' | awk -F "<" '{print $1}' | awk '{print $1}')
gateway=$(ip route | grep default | awk '{print $3}')
mac_info=$(ip link | egrep -v "lo" | grep link | awk '{print $2}')
dns_config=$(egrep 'nameserver' /etc/resolv.conf)
route_info=$(route -n)
echo -e ' IP信息'
cat <<EOF | column -t
系统公网地址: ${pub_ipadd}
系统私网地址: ${pri_ipadd}
网关地址: ${gateway}
MAC地址: ${mac_info}
路由信息:
${route_info}
DNS 信息:
${dns_config}
EOF
}
# 获取系统磁盘信息
function get_disk_info() {
disk_info=$(fdisk -l | grep "Disk /dev" | cut -d, -f1)
disk_use=$(df -hTP | awk '$2!="tmpfs"{print}')
disk_inode=$(df -hiP | awk '$1!="tmpfs"{print}')
echo -e ' 磁盘信息:'
cat <<EOF
${disk_info}
磁盘使用:
${disk_use}
inode信息:
${disk_inode}
EOF
}
# 获取系统信息
function get_systatus_info() {
sys_os=$(uname -o)
sys_release=$(cat /etc/redhat-release)
sys_kernel=$(uname -r)
sys_hostname=$(hostname)
sys_selinux=$(getenforce)
sys_lang=$(echo $LANG)
sys_lastreboot=$(who -b | awk '{print $3,$4}')
sys_runtime=$(uptime | awk '{print $3,$4}' | cut -d, -f1)
sys_time=$(date)
sys_load=$(uptime | cut -d: -f5)
echo -e ' 系统信息:'
cat <<EOF | column -t
系统: ${sys_os}
发行版本: ${sys_release}
系统内核: ${sys_kernel}
主机名: ${sys_hostname}
selinux状态: ${sys_selinux}
系统语言: ${sys_lang}
系统当前时间: ${sys_time}
系统最后重启时间: ${sys_lastreboot}
系统运行时间: ${sys_runtime}
系统负载: ${sys_load}
---------------------------------------
EOF
}
# 获取服务信息
function get_service_info() {
port_listen=$(netstat -lntup | grep -v "Active Internet")
kernel_config=$(sysctl -p 2>/dev/null)
if [ ${sysversion} -gt 6 ]; then
service_config=$(systemctl list-unit-files --type=service --state=enabled | grep "enabled")
run_service=$(systemctl list-units --type=service --state=running | grep ".service")
else
service_config=$(/sbin/chkconfig | grep -E ":on|:启用" | column -t)
run_service=$(/sbin/service --status-all | grep -E "running")
fi
echo -e ' 服务启动配置:'
cat <<EOF
${service_config}
${line}
运行的服务:
${run_service}
${line}
监听端口:
${port_listen}
${line}
内核参考配置:
${kernel_config}
EOF
}
function get_sys_user() {
login_user=$(awk -F: '{if ($NF=="/bin/bash") print $0}' /etc/passwd)
ssh_config=$(egrep -v "^#|^$" /etc/ssh/sshd_config)
sudo_config=$(egrep -v "^#|^$" /etc/sudoers | grep -v "^Defaults")
host_config=$(egrep -v "^#|^$" /etc/hosts)
crond_config=$(for cronuser in /var/spool/cron/*; do
ls ${cronuser} 2>/dev/null | cut -d/ -f5
egrep -v "^$|^#" ${cronuser} 2>/dev/null
echo ""
done)
echo -e ' 系统登录用户:'
cat <<EOF
${login_user}
${line}
ssh 配置信息:
${ssh_config}
${line}
sudo 配置用户:
${sudo_config}
${line}
定时任务配置:
${crond_config}
${line}
hosts 信息:
${host_config}
EOF
}
function process_top_info() {
top_title=$(top -b n1 | head -7 | tail -1)
cpu_top10=$(top b -n1 | head -17 | tail -10)
mem_top10=$(top -b n1 | head -17 | tail -10 | sort -k10 -r)
echo -e ' CPU占用top10'
cat <<EOF
${top_title}
${cpu_top10}
EOF
echo -e ' 内存占用top10'
cat <<EOF
${top_title}
${mem_top10}
EOF
}
function sys_check() {
get_systatus_info
echo ${line}
get_cpu_info
echo ${line}
get_mem_info
echo ${line}
# get_net_info
# echo ${line}
get_disk_info
echo ${line}
get_service_info
echo ${line}
# get_sys_user
# echo ${line}
process_top_info
}
sys_check

View File

@@ -0,0 +1,41 @@
# 获取系统cpu信息
function get_cpu_info() {
Physical_CPUs=$(grep "physical id" /proc/cpuinfo | sort | uniq | wc -l)
Virt_CPUs=$(grep "processor" /proc/cpuinfo | wc -l)
CPU_Kernels=$(grep "cores" /proc/cpuinfo | uniq | awk -F ': ' '{print $2}')
CPU_Type=$(grep "model name" /proc/cpuinfo | awk -F ': ' '{print $2}' | sort | uniq)
CPU_Arch=$(uname -m)
echo -e '\n-------------------------- CPU信息 --------------------------'
cat <<EOF | column -t
物理CPU个数: $Physical_CPUs
逻辑CPU个数: $Virt_CPUs
每CPU核心数: $CPU_Kernels
CPU型号: $CPU_Type
CPU架构: $CPU_Arch
EOF
}
# 获取系统信息
function get_systatus_info() {
sys_os=$(uname -o)
sys_release=$(cat /etc/redhat-release)
sys_kernel=$(uname -r)
sys_hostname=$(hostname)
sys_selinux=$(getenforce)
sys_lang=$(echo $LANG)
sys_lastreboot=$(who -b | awk '{print $3,$4}')
echo -e '-------------------------- 系统信息 --------------------------'
cat <<EOF | column -t
系统: ${sys_os}
发行版本: ${sys_release}
系统内核: ${sys_kernel}
主机名: ${sys_hostname}
selinux状态: ${sys_selinux}
系统语言: ${sys_lang}
系统最后重启时间: ${sys_lastreboot}
EOF
}
get_systatus_info
#echo -e "\n"
get_cpu_info

105
machine/status.go Normal file
View File

@@ -0,0 +1,105 @@
package machine
import (
"mayfly-go/base"
"mayfly-go/base/utils"
"strconv"
"strings"
)
type SystemVersion struct {
Version string
}
func GetSystemVersion(cli *Cli) *SystemVersion {
res := cli.Run("cat /etc/redhat-release")
return &SystemVersion{
Version: res,
}
}
//top - 17:14:07 up 5 days, 6:30, 2 users, load average: 0.03, 0.04, 0.05
//Tasks: 101 total, 1 running, 100 sleeping, 0 stopped, 0 zombie
//%Cpu(s): 6.2 us, 0.0 sy, 0.0 ni, 93.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
//KiB Mem : 1882012 total, 73892 free, 770360 used, 1037760 buff/cache
//KiB Swap: 0 total, 0 free, 0 used. 933492 avail Mem
type Top struct {
Time string `json:"time"`
// 从本次开机到现在经过的时间
Up string `json:"up"`
// 当前有几个用户登录到该机器
NowUsers int `json:"nowUsers"`
// load average: 0.03, 0.04, 0.05 (系统1分钟、5分钟、15分钟内的平均负载值)
OneMinLoadavg float32 `json:"oneMinLoadavg"`
FiveMinLoadavg float32 `json:"fiveMinLoadavg"`
FifteenMinLoadavg float32 `json:"fifteenMinLoadavg"`
// 进程总数
TotalTask int `json:"totalTask"`
// 正在运行的进程数对应状态TASK_RUNNING
RunningTask int `json:"runningTask"`
SleepingTask int `json:"sleepingTask"`
StoppedTask int `json:"stoppedTask"`
ZombieTask int `json:"zombieTask"`
// 进程在用户空间user消耗的CPU时间占比不包含调整过优先级的进程
CpuUs float32 `json:"cpuUs"`
// 进程在内核空间system消耗的CPU时间占比
CpuSy float32 `json:"cpuSy"`
// 调整过用户态优先级的niced进程的CPU时间占比
CpuNi float32 `json:"cpuNi"`
// 空闲的idleCPU时间占比
CpuId float32 `json:"cpuId"`
// 等待waitI/O完成的CPU时间占比
CpuWa float32 `json:"cpuWa"`
// 处理硬中断hardware interrupt的CPU时间占比
CpuHi float32 `json:"cpuHi"`
// 处理硬中断hardware interrupt的CPU时间占比
CpuSi float32 `json:"cpuSi"`
// 当Linux系统是在虚拟机中运行时等待CPU资源的时间steal time占比
CpuSt float32 `json:"cpuSt"`
TotalMem int `json:"totalMem"`
FreeMem int `json:"freeMem"`
UsedMem int `json:"usedMem"`
CacheMem int `json:"cacheMem"`
TotalSwap int `json:"totalSwap"`
FreeSwap int `json:"freeSwap"`
UsedSwap int `json:"usedSwap"`
AvailMem int `json:"availMem"`
}
func GetTop(cli *Cli) *Top {
res := cli.Run("top -b -n 1 | head -5")
topTemp := "top - {upAndUsers}, load average: {loadavg}\n" +
"Tasks:{totalTask} total,{runningTask} running,{sleepingTask} sleeping,{stoppedTask} stopped,{zombieTask} zombie\n" +
"%Cpu(s):{cpuUs} us,{cpuSy} sy,{cpuNi} ni,{cpuId} id,{cpuWa} wa,{cpuHi} hi,{cpuSi} si,{cpuSt} st\n" +
"KiB Mem :{totalMem} total,{freeMem} free,{usedMem} used,{cacheMem} buff/cache\n" +
"KiB Swap:{totalSwap} total,{freeSwap} free,{usedSwap} used. {availMem} avail Mem \n"
resMap := make(map[string]interface{})
utils.ReverStrTemplate(topTemp, res, resMap)
//17:14:07 up 5 days, 6:30, 2
timeUpAndUserStr := resMap["upAndUsers"].(string)
timeUpAndUser := strings.Split(timeUpAndUserStr, "up")
time := utils.StrTrim(timeUpAndUser[0])
upAndUsers := strings.Split(timeUpAndUser[1], ",")
up := utils.StrTrim(upAndUsers[0]) + upAndUsers[1]
users, _ := strconv.Atoi(utils.StrTrim(strings.Split(utils.StrTrim(upAndUsers[2]), " ")[0]))
// 0.03, 0.04, 0.05
loadavgs := strings.Split(resMap["loadavg"].(string), ",")
oneMinLa, _ := strconv.ParseFloat(loadavgs[0], 32)
fiveMinLa, _ := strconv.ParseFloat(utils.StrTrim(loadavgs[1]), 32)
fifMinLa, _ := strconv.ParseFloat(utils.StrTrim(loadavgs[2]), 32)
top := &Top{Time: time, Up: up, NowUsers: users, OneMinLoadavg: float32(oneMinLa), FiveMinLoadavg: float32(fiveMinLa), FifteenMinLoadavg: float32(fifMinLa)}
err := utils.Map2Struct(resMap, top)
base.BizErrIsNil(err, "解析top出错")
return top
}
type Status struct {
// 系统版本
SysVersion SystemVersion
// top信息
Top Top
}

261
machine/ws_shell_session.go Normal file
View File

@@ -0,0 +1,261 @@
package machine
import (
"bytes"
"encoding/json"
"github.com/astaxie/beego/logs"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
"io"
"sync"
"time"
)
//func WsSsh(c *controllers.MachineController) {
// wsConn, err := upGrader.Upgrade(c.Ctx.ResponseWriter, c.Ctx.Request, nil)
// if err != nil {
// panic(base.NewBizErr("获取requst responsewirte错误"))
// }
//
// cols, _ := c.GetInt("col", 80)
// rows, _ := c.GetInt("rows", 40)
//
// sws, err := NewLogicSshWsSession(cols, rows, true, GetCli(c.GetMachineId()).client, wsConn)
// if sws == nil {
// panic(base.NewBizErr("连接失败"))
// }
// //if wshandleError(wsConn, err) {
// // return
// //}
// defer sws.Close()
//
// quitChan := make(chan bool, 3)
// sws.Start(quitChan)
// go sws.Wait(quitChan)
//
// <-quitChan
// //保存日志
//
// ////write logs
// //xtermLog := model.SshLog{
// // StartedAt: startTime,
// // UserId: userM.Id,
// // Log: sws.LogString(),
// // MachineId: idx,
// // ClientIp: cIp,
// //}
// //err = xtermLog.Create()
// //if wshandleError(wsConn, err) {
// // return
//}
type safeBuffer struct {
buffer bytes.Buffer
mu sync.Mutex
}
func (w *safeBuffer) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func (w *safeBuffer) Bytes() []byte {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Bytes()
}
func (w *safeBuffer) Reset() {
w.mu.Lock()
defer w.mu.Unlock()
w.buffer.Reset()
}
const (
wsMsgCmd = "cmd"
wsMsgResize = "resize"
)
type wsMsg struct {
Type string `json:"type"`
Cmd string `json:"cmd"`
Cols int `json:"cols"`
Rows int `json:"rows"`
}
type LogicSshWsSession struct {
stdinPipe io.WriteCloser
comboOutput *safeBuffer //ssh 终端混合输出
logBuff *safeBuffer //保存session的日志
inputFilterBuff *safeBuffer //用来过滤输入的命令和ssh_filter配置对比的
session *ssh.Session
wsConn *websocket.Conn
isAdmin bool
IsFlagged bool `comment:"当前session是否包含禁止命令"`
}
func NewLogicSshWsSession(cols, rows int, isAdmin bool, cli *Cli, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
sshSession, err := cli.GetSession()
if err != nil {
return nil, err
}
stdinP, err := sshSession.StdinPipe()
if err != nil {
return nil, err
}
comboWriter := new(safeBuffer)
logBuf := new(safeBuffer)
inputBuf := new(safeBuffer)
//ssh.stdout and stderr will write output into comboWriter
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
modes := ssh.TerminalModes{
ssh.ECHO: 1, // disable echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
return nil, err
}
// Start remote shell
if err := sshSession.Shell(); err != nil {
return nil, err
}
//sshSession.Run("top")
return &LogicSshWsSession{
stdinPipe: stdinP,
comboOutput: comboWriter,
logBuff: logBuf,
inputFilterBuff: inputBuf,
session: sshSession,
wsConn: wsConn,
isAdmin: isAdmin,
IsFlagged: false,
}, nil
}
//Close 关闭
func (sws *LogicSshWsSession) Close() {
if sws.session != nil {
sws.session.Close()
}
if sws.logBuff != nil {
sws.logBuff = nil
}
if sws.comboOutput != nil {
sws.comboOutput = nil
}
}
func (sws *LogicSshWsSession) Start(quitChan chan bool) {
go sws.receiveWsMsg(quitChan)
go sws.sendComboOutput(quitChan)
}
//receiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin
func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
wsConn := sws.wsConn
//tells other go routine quit
defer setQuit(exitCh)
for {
select {
case <-exitCh:
return
default:
//read websocket msg
_, wsData, err := wsConn.ReadMessage()
if err != nil {
logs.Error("reading webSocket message failed")
//panic(base.NewBizErr("reading webSocket message failed"))
return
}
//unmashal bytes into struct
msgObj := wsMsg{}
if err := json.Unmarshal(wsData, &msgObj); err != nil {
logs.Error("unmarshal websocket message failed")
//panic(base.NewBizErr("unmarshal websocket message failed"))
}
switch msgObj.Type {
case wsMsgResize:
//handle xterm.js size change
if msgObj.Cols > 0 && msgObj.Rows > 0 {
if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
logs.Error("ssh pty change windows size failed")
//panic(base.NewBizErr("ssh pty change windows size failed"))
}
}
case wsMsgCmd:
//handle xterm.js stdin
//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
//if err != nil {
// logs.Error("websock cmd string base64 decoding failed")
// //panic(base.NewBizErr("websock cmd string base64 decoding failed"))
//}
sws.sendWebsocketInputCommandToSshSessionStdinPipe([]byte(msgObj.Cmd))
}
}
}
}
//sendWebsocketInputCommandToSshSessionStdinPipe
func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
if _, err := sws.stdinPipe.Write(cmdBytes); err != nil {
logs.Error("ws cmd bytes write to ssh.stdin pipe failed")
//panic(base.NewBizErr("ws cmd bytes write to ssh.stdin pipe failed"))
}
}
func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
wsConn := sws.wsConn
//todo 优化成一个方法
//tells other go routine quit
defer setQuit(exitCh)
//every 120ms write combine output bytes into websocket response
tick := time.NewTicker(time.Millisecond * time.Duration(60))
//for range time.Tick(120 * time.Millisecond){}
defer tick.Stop()
for {
select {
case <-tick.C:
if sws.comboOutput == nil {
return
}
bs := sws.comboOutput.Bytes()
if len(bs) > 0 {
err := wsConn.WriteMessage(websocket.TextMessage, bs)
if err != nil {
logs.Error("ssh sending combo output to webSocket failed")
//panic(base.NewBizErr("ssh sending combo output to webSocket failed"))
}
_, err = sws.logBuff.Write(bs)
if err != nil {
logs.Error("combo output to log buffer failed")
//panic(base.NewBizErr("combo output to log buffer failed"))
}
sws.comboOutput.buffer.Reset()
}
case <-exitCh:
return
}
}
}
func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
if err := sws.session.Wait(); err != nil {
logs.Error("ssh session wait failed")
//panic(base.NewBizErr("ssh session wait failed"))
setQuit(quitChan)
}
}
func (sws *LogicSshWsSession) LogString() string {
return sws.logBuff.buffer.String()
}
func setQuit(ch chan bool) {
ch <- true
}

42
main.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
"github.com/astaxie/beego/orm"
"github.com/astaxie/beego/plugins/cors"
_ "github.com/go-sql-driver/mysql"
_ "mayfly-go/routers"
scheduler "mayfly-go/scheudler"
"net/http"
"strings"
)
func init() {
orm.RegisterDriver("mysql", orm.DRMySQL)
orm.RegisterDataBase("default", "mysql", "root:111049@tcp(localhost:3306)/mayfly-job?charset=utf8&loc=Local")
}
func main() {
orm.Debug = true
// 跨域配置
beego.InsertFilter("/**", beego.BeforeRouter, cors.Allow(&cors.Options{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"},
ExposeHeaders: []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"},
AllowCredentials: true,
}))
scheduler.Start()
defer scheduler.Stop()
beego.Run()
}
// 解决beego无法访问根目录静态文件
func TransparentStatic(ctx *context.Context) {
if strings.Index(ctx.Request.URL.Path, "api/") >= 0 {
return
}
http.ServeFile(ctx.ResponseWriter, ctx.Request, "static/"+ctx.Request.URL.Path)
}

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@@ -0,0 +1,5 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = 'http://localhost:8888/api'

View File

@@ -0,0 +1,5 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = 'http://localhost:8888/api'

View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

23
mayfly-go-front/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
mayfly-go-front/README.md Normal file
View File

@@ -0,0 +1,24 @@
# mayfly-go-front
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

12584
mayfly-go-front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "mayfly-go-front",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@types/echarts": "^4.6.4",
"axios": "^0.19.2",
"core-js": "^3.6.5",
"echarts": "^4.8.0",
"element-ui": "^2.13.2",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^8.4.2",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"typescript": "~3.9.3",
"vue-template-compiler": "^2.6.11",
"sass-resources-loader": "^2.0.3",
"ts-import-plugin": "^1.6.6",
"less": "^3.10.3",
"less-loader": "^5.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,11 @@
<template>
<div id="app">
<router-view/>
</div>
</template>
<style lang="scss">
#app {
background-color: #222d32;
}
</style>

View File

@@ -0,0 +1,75 @@
* {
padding: 0;
margin: 0;
outline: none;
box-sizing: border-box;
}
body{
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
}
a {
color: #3c8dbc;
text-decoration: none;
}
::-webkit-scrollbar {
width: 4px;
height: 8px;
background-color: #F5F5F5;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #F5F5F5;
}
::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #F5F5F5;
}
.el-menu .fa {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
}
.el-menu .fa:not(.is-children) {
font-size: 14px;
}
.gray-mode{
filter: grayscale(100%);
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
/* 元素无法被选择 */
.none-select {
moz-user-select: -moz-none;
-moz-user-select: none;
-o-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.toolbar {
width: 100%;
padding: 8px;
background-color: #ffffff;
overflow: hidden;
line-height: 32px;
border: 1px solid #e6ebf5;
}
.fl {
float: left;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,77 @@
import request from './request'
/**
* 可用于各模块定义各自api请求
*/
class Api {
/**
* 请求url
*/
url: string;
/**
* 请求方法
*/
method: string;
constructor(url: string, method: string) {
this.url = url;
this.method = method;
}
/**
* 设置rl
* @param {String} uri 请求url
*/
setUrl(url: string) {
this.url = url;
return this;
}
/**
* url的请求方法
* @param {String} method 请求方法
*/
setMethod(method: string) {
this.method = method;
return this;
}
/**
* 获取权限的完整url
*/
getUrl() {
return request.getApiUrl(this.url);
}
/**
* 操作该权限即请求对应的url
* @param {Object} param 请求该权限的参数
*/
request(param: any): Promise<any> {
return request.send(this, param);
}
/**
* 操作该权限即请求对应的url
* @param {Object} param 请求该权限的参数
*/
requestWithHeaders(param: any, headers: any): Promise<any> {
return request.sendWithHeaders(this, param, headers);
}
/** 静态方法 **/
/**
* 静态工厂返回Api对象并设置url与method属性
* @param url url
* @param method 请求方法(get,post,put,delete...)
*/
static create(url: string, method: string) {
return new Api(url, method);
}
}
export default Api

View File

@@ -0,0 +1,26 @@
export class AuthUtils {
private static tokenName = 'token'
/**
* 保存token
* @param token token
*/
static saveToken(token: string) {
sessionStorage.setItem(this.tokenName, token)
}
/**
* 获取token
*/
static getToken() {
return sessionStorage.getItem(this.tokenName)
}
/**
* 移除token
*/
static removeToken() {
sessionStorage.removeItem(this.tokenName)
}
}

View File

@@ -0,0 +1,5 @@
const config = {
baseApiUrl: process.env.VUE_APP_BASE_API
}
export default config

View File

@@ -0,0 +1,27 @@
interface BaseEnum {
name: string
value: any
}
const success: BaseEnum = {
name: 'success',
value: 200
}
export enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501
}
// /**
// * 全局公共枚举类
// */
// export default {
// // uri请求方法
// requestMethod: new Enum().add('GET', 'GET', 1).add('POST', 'POST', 2).add('PUT', 'PUT', 3).add('DELETE', 'DELETE', 4),
// // 结果枚举
// ResultEnum: new Enum().add('SUCCESS', '操作成功', 200).add('ERROR', '操作失败', 400).add('PARAM_ERROR', '参数错误', 405).add('SERVER_ERROR', '服务器异常', 500)
// .add('NO_PERMISSION', '没有权限', 501)
// }

View File

@@ -0,0 +1,105 @@
/*
* @Date: 2020-05-23 09:55:10
* @LastEditors: JOU(wx: huzhen555)
* @LastEditTime: 2020-05-27 15:34:15
*/
import { time2Date } from '@/common/util';
/**
* @description: 格式化时间过滤器
* @author: JOU(wx: huzhen555)
* @param {any} value 过滤器参数
* @return: 转换后的参数
*/
function timeStr2Date(value: string) {
return time2Date(value);
}
/**
* @description: 以一个分隔符替换为另一个分隔符,常用于数组字符串转换为某格式
* @author: JOU(wx: huzhen555)
* @param {any} value 过滤器参数
* @return: 转换后的参数
*/
function replaceTag(value: string, newSep = '', oldSep = ',') {
return value.replace(new RegExp(oldSep, 'g'), () => newSep);
}
/**
* @description: 字符串转数组
* @author: JOU(wx: huzhen555)
* @param {string} value 待转换字符串
* @return: 转换后的数组
*/
function str2Ary(value: string, sep = ',') {
return (value || '').split(sep);
}
/**
* @description: 按shopName(subName)格式化店名
* @author: JOU(wx: huzhen555)
* @param {string} value 待转化字符串
* @return: 格式化后的店名
*/
function formatShopName(value: string, subName = '') {
if (subName) {
return `${value}(${subName})`;
}
return value;
}
export const vueFilters = {
timeStr2Date, replaceTag, formatShopName,
};
// /**
// * @description: 返回数据的格式化,如有些数据需要以逗号隔开转换成数组等
// * @author: JOU(wx: huzhen555)
// * @param {any} data 格式化的数据
// * @param {any} rules 转换规则可对传入objectstringfunction
// * object时data必需为array格式为 { key1: ['filterName', 'arg1', 'arg2'], key2: function <= [自定义过滤器] }
// * string时表示某个过滤器的方法名
// * function时表示某个自定义过滤器
// * @return: 转换后的数据
// */
// const filterHandlers = { ...vueFilters, str2Ary };
// type TCustomerFilter = (...args: any[]) => any;
// type TRuleMap = IGeneralObject<[string, ...any[]]|TCustomerFilter>
// export function formatResp(data: any, rules: TRuleMap|TCustomerFilter|string) {
// const ruleHandler = (rule: TCustomerFilter|string, dataItem: any, origin: any[]) => {
// if (typeof rule === 'string' && typeof filterHandlers[rule] === 'function') {
// dataItem = filterHandlers[rule](dataItem);
// }
// else if (Array.isArray(rule) && rule.length > 0 && typeof filterHandlers[rule[0]] === 'function') {
// dataItem = filterHandlers[rule[0]].apply([dataItem, ...rule.slice(1)]);
// }
// else if (typeof rule === 'function') {
// dataItem = rule(dataItem, origin);
// }
// return dataItem;
// }
// if (Array.isArray(data)) {
// if (data.length <= 0 || Object.keys(rules).length <= 0) {
// return data;
// }
// return data.map(dataItem => {
// rules = rules as TRuleMap;
// for (let ruleKey in rules) {
// let rule = rules[ruleKey];
// dataItem[ruleKey] = ruleHandler(rule, dataItem[ruleKey], dataItem);
// }
// return dataItem;
// });
// }
// else if (typeof rules === 'string' || typeof rules === 'function') {
// return ruleHandler(rules, data, data);
// }
// else {
// return data;
// }
// }

View File

@@ -0,0 +1,7 @@
import request from './request'
export default {
login: (param: any) => request.request('POST', '/accounts/login', param, null),
captcha: () => request.request('GET', '/open/captcha', null, null),
logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param, null)
}

View File

@@ -0,0 +1,183 @@
import router from "../router";
import Axios from 'axios';
import { ResultEnum } from './enums'
import Api from './Api';
import { AuthUtils } from './AuthUtils'
import config from './config';
import ElementUI from 'element-ui';
export interface Result {
/**
* 响应码
*/
code: number;
/**
* 响应消息
*/
msg: string;
/**
* 数据
*/
data?: any;
}
const baseUrl = config.baseApiUrl
/**
* 通知错误消息
* @param msg 错误消息
*/
function notifyErrorMsg(msg: string) {
// 危险通知
ElementUI.Message.error(msg);
}
// create an axios instance
const service = Axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 20000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
const token = AuthUtils.getToken()
if (token) {
// 设置token
config.headers['Authorization'] = token
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
response => {
// 获取请求返回结果
const data: Result = response.data;
// 如果提示没有权限则移除token使其重新登录
if (data.code === ResultEnum.NO_PERMISSION) {
AuthUtils.removeToken()
notifyErrorMsg('登录超时')
setTimeout(() => {
router.push({
path: '/login',
});
}, 1000)
return;
}
if (data.code === ResultEnum.SUCCESS) {
return data.data;
} else {
return Promise.reject(data);
}
},
( error: any) => {
return Promise.reject(error)
}
)
/**
* @author: hml
*
* 将带有{id}的url替换为真实值
* 若restUrl:/category/{categoryId}/product/{productId} param:{categoryId:1, productId:2}
* 则返回 /category/1/product/2 的url
*/
function parseRestUrl(restUrl: string, param: any) {
return restUrl.replace(/\{\w+\}/g, (word) => {
const key = word.substring(1, word.length - 1);
const value = param[key];
if (value != null || value != undefined) {
// delete param[key]
return value;
}
return "";
});
}
/**
* 请求uri
* 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
*
* @param {Object} method 请求方法(GET,POST,PUT,DELTE等)
* @param {Object} uri uri
* @param {Object} params 参数
*/
function request(method: string, url: string, params: any, headers: any): Promise<any> {
if (!url)
throw new Error('请求url不能为空');
// 简单判断该url是否是restful风格
if (url.indexOf("{") != -1) {
url = parseRestUrl(url, params);
}
const query: any = {
method,
url: url,
};
if (headers) {
query.headers = headers
}
// else {
// query.headers = {}
// }
const lowMethod = method.toLowerCase();
// const signKey = 'sd8mow3RPMDS0PMPmMP98AS2RG43T'
// if (params) {
// delete params.sign
// query.headers = headers || {}
// // query.headers.sign = md5(Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&') + signKey)
// } else {
// query.headers = headers || {}
// query.headers.sign = {'sign': md5(signKey)}
// }
// post和put使用json格式传参
if (lowMethod === 'post' || lowMethod === 'put') {
query.data = params;
// query.headers.sign = md5(JSON.stringify(params) + signKey)
} else {
query.params = params;
// query.headers.sign = md5(Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&') + signKey)
}
return service.request(query).then(res => res)
.catch(e => {
notifyErrorMsg(e.msg || e.message)
return Promise.reject(e);
});
}
/**
* 根据api执行对应接口
* @param api Api实例
* @param params 请求参数
*/
function send(api: Api, params: any): Promise<any> {
return request(api.method, api.url, params, null);
}
/**
* 根据api执行对应接口
* @param api Api实例
* @param params 请求参数
*/
function sendWithHeaders(api: Api, params: any, headers: any): Promise<any> {
return request(api.method, api.url, params, headers);
}
function getApiUrl(url: string) {
// 只是返回api地址而不做请求用在上传组件之类的
return baseUrl + url + '?token=' + AuthUtils.getToken();
}
export default {
request,
send,
sendWithHeaders,
parseRestUrl,
getApiUrl
}

View File

@@ -0,0 +1,96 @@
/**
* 时间字符串转成具体日期,数据库里的时间戳可直接传入转换
* @author JOU
* @time 2019-03-31T21:58:06+0800
* @param {number} timeStr 时间字符串
* @return {string} 转换后的具体时间日期
*/
export function time2Date(timeStr: string) {
if (timeStr === '2100-01-01 00:00:00') {
return '长期';
}
const
ts = new Date(timeStr).getTime() / 1000,
dateObj = new Date(),
tsn = Date.parse(dateObj.toString()) / 1000,
timeGap = tsn - ts,
oneDayTs = 24 * 60 * 60,
oneHourTs = 60 * 60,
oneMinuteTs = 60,
fillZero = (num: number) => num >= 0 && num < 10 ? ('0' + num) : num.toString(),
getTimestamp = (dateObj: Date) => Date.parse(dateObj.toString()) / 1000;
// 未来的时间1天后的显示“xx天后”
if (timeGap < -oneDayTs) {
return Math.floor(-timeGap / oneDayTs) + '天后';
}
// 未来不到一天的时间显示“xx小时后”
if (timeGap > -oneDayTs && timeGap < -oneHourTs) {
return Math.floor(-timeGap / oneHourTs) + '小时后';
}
// 未来不到一小时的时间显示“xx分钟后”
if (timeGap > -oneHourTs && timeGap < 0) {
return Math.floor(-timeGap / oneMinuteTs) + '小时后';
}
// 十分钟前返回“刚刚”
if (timeGap < (oneMinuteTs * 10)) {
return '刚刚';
}
// 一小时前显示“xx分钟前”
if (timeGap < oneHourTs) {
return `${Math.floor(timeGap / oneMinuteTs)}分钟前`;
}
// 当天的显示”xx小时前“
dateObj.setHours(0, 0, 0, 0);
if (timeGap < tsn - getTimestamp(dateObj)) {
return `${Math.floor(timeGap / oneHourTs)}小时前`;
}
// 昨天显示”昨天 xx:xx“
const
date = dateObj.getDate(),
d = new Date(ts * 1000);
dateObj.setDate(date - 1);
if (timeGap < tsn - getTimestamp(dateObj)) {
return `昨天 ${fillZero(d.getHours())}:${fillZero(d.getMinutes())}`;
}
// 前天显示”前天 xx:xx“
dateObj.setDate(date - 2);
if (timeGap < tsn - getTimestamp(dateObj)) {
return `前天 ${fillZero(d.getHours())}:${fillZero(d.getMinutes())}`;
}
// 这周显示”这周x xx:xx“
// 因为上面减了两天,需设置回去
dateObj.setDate(date);
let currentDay = dateObj.getDay(), day = d.getDay();
const weeks = [ '一', '二', '三', '四', '五', '六', '天' ];
currentDay = currentDay === 0 ? 7 : currentDay;
day = day === 0 ? 7 : day;
dateObj.setDate(date - currentDay + 1);
if (timeGap < tsn - getTimestamp(dateObj)) {
return `这周${weeks[day - 1]} ${fillZero(d.getHours())}:${fillZero(d.getMinutes())}`;
}
// 上周显示”上周x xx:xx“
dateObj.setDate(date - 6 - currentDay);
if (timeGap < tsn - getTimestamp(dateObj)) {
return `上周${weeks[day - 1]} ${fillZero(d.getHours())}:${fillZero(d.getMinutes())}`;
}
// 今年再往前的日期则显示”xx-xx xx:xx“表示xx月xx日 xx点xx分
dateObj.setMonth(0, 1);
if (timeGap < tsn - getTimestamp(dateObj)) {
return `${fillZero(d.getMonth() + 1)}-${fillZero(d.getDate())} ${fillZero(d.getHours())}:${fillZero(d.getMinutes())}`;
}
return `${d.getFullYear()}-${fillZero(d.getMonth() + 1)}-${fillZero(d.getDate())} ${fillZero(d.getHours())}:${fillZero(d.getMinutes())}`;
}

View File

@@ -0,0 +1,64 @@
<template>
<div class="active-plate-main">
<ul class="active-list">
<li class="item" v-for="item in infoList" :key="item.title">
<p class="num" :style="{color:item.color}">{{item.count}}</p>
<p class="desc">{{item.title}}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'activePlate',
components: {},
props: {
// 需要展示的数据集合
infoList: {
type: Array,
require: true,
},
},
}
</script>
<style lang="less">
.active-plate-main {
width: 100%;
height: 130px;
.active-list {
display: flex;
list-style: none;
padding-top: 15px;
.item {
position: relative;
flex: 1;
text-align: center;
.num {
font-size: 42px;
font-weight: bold;
font-family: sans-serif;
}
.desc {
font-size: 16px;
}
&::after {
position: absolute;
top: 18px;
right: 0;
content: '';
display: block;
width: 1px;
height: 56px;
background: #e7eef0;
}
&:nth-last-of-type(1) {
&::after {
background: none;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="base-chart" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
option: Object,
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(this.option)
on(window, 'resize', this.resize)
})
},
},
}
</script>
<style>
.base-chart {
width: 100%;
height: 360px;
padding: 28px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="card-main">
<div class="title">
{{title}}
<span>{{desc}}</span>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '标题'
},
desc: {
type: String,
default: '描述'
}
}
}
</script>
<style lang='less'>
.card-main {
border-radius: 8px;
background: #fff;
margin-bottom: 20px;
padding-bottom: 10px;
}
.title {
color: #060606;
font-size: 16px;
padding: 20px 32px;
span {
padding-left: 17px;
font-size: 12px;
color: #dededf;
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="bar-main" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
value: Object,
text: String,
subtext: String
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
const xAxisData = Object.keys(this.value)
const seriesData = Object.values(this.value)
const option = {
grid: {
left: '1%',
right: '1%',
top: '2%',
bottom: '1%',
containLabel: true
},
title: {
text: this.text,
subtext: this.subtext,
x: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{c}人',
// position: ['30%', '90%'],
position: 'top',
backgroundColor: '#FAFBFE',
textStyle: {
fontSize: 14,
color: '#6d6d6d'
}
},
xAxis: {
// show: false,
type: 'category',
data: xAxisData,
splitLine: {
show: false
}
},
yAxis: [
{
// show: false,
type: 'value',
splitLine: {
show: true,
lineStyle: {
// 设置刻度线粗度(粗的宽度)
width: 1,
// 颜色数组,数组数量要比刻度线数量大才能不循环使用
color: [
'rgba(0, 0, 0, 0)',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee'
]
}
}
}
],
series: [
{
data: seriesData,
type: 'bar',
barWidth: 36,
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#f2f5ff' },
{ offset: 1, color: '#fff' }
])
}
},
itemStyle: {
normal: {
barBorderRadius: [50],
color: new echarts.graphic.LinearGradient(
0,
1,
0,
0,
[
{
offset: 0,
color: '#3AA1FF' // 0% 处的颜色
},
{
offset: 1,
color: '#36CBCB' // 100% 处的颜色
}
],
false
)
}
}
}
]
}
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(option)
on(window, 'resize', this.resize)
})
}
}
}
</script>
<style>
.bar-main {
width: 100%;
height: 360px;
padding: 28px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<div class="line-main" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
value: Array,
title: String,
subtext: String,
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
const dateList = this.value.map(function (item) {
return item[0]
})
const valueList = this.value.map(function (item) {
return item[1]
})
const option = {
// Make gradient line here
visualMap: [
{
show: false,
type: 'continuous',
seriesIndex: 0,
min: 0,
max: 400,
}
],
title: [
{
left: 'center',
text: this.title,
}
],
tooltip: {
trigger: 'axis',
},
xAxis: [
{
data: dateList,
}
],
yAxis: [
{
splitLine: { show: false },
},
],
grid: [
{
},
],
series: [
{
type: 'line',
showSymbol: false,
data: valueList,
},
],
}
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(option)
on(window, 'resize', this.resize)
})
},
},
}
</script>
<style>
.line-main {
width: 100%;
height: 360px;
padding: 28px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="funnel-main" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
value: Array,
text: String,
subtext: String
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
const legend = this.value.map(_ => _.name)
const option = {
grid: {
left: '1%',
right: '1%',
top: '2%',
bottom: '1%',
containLabel: true
},
title: {
text: this.text,
subtext: this.subtext,
x: 'center'
},
tooltip: {
show: false,
trigger: 'item',
formatter: '{c} ({d}%)',
// position: ['30%', '90%'],
position: 'right',
backgroundColor: 'transparent',
textStyle: {
fontSize: 14,
color: '#666'
}
},
legend: {
orient: 'vertical',
left: 'right',
bottom: 0,
// data: legend,
backgroundColor: 'transparent',
icon: 'circle'
},
series: [
{
name: '访问来源',
type: 'funnel',
radius: ['50%', '65%'],
avoidLabelOverlap: false,
label: {
normal: {
show: false,
position: 'right',
formatter: '{c} ({d}%)'
}
},
// labelLine: {
// normal: {
// show: false
// }
// },
data: [
{ value: 400, name: '交易完成' },
{ value: 300, name: '支付订单' },
{ value: 200, name: '生成订单' },
{ value: 100, name: '放入购物车' },
{ value: 100, name: '浏览网站' }
]
}
]
}
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(option)
on(window, 'resize', this.resize)
})
}
}
}
</script>
<style>
.funnel-main {
width: 100%;
height: 295px;
padding: 28px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div class="gauge-main" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
value: Object,
text: String,
subtext: String
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
const option = {
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0
// containLabel: true
},
tooltip: {
formatter: '{a} <br/>{b} : {c}%'
},
toolbox: {},
series: [
{
name: '业务指标',
startAngle: 195,
endAngle: -15,
axisLine: {
show: true,
lineStyle: {
color: [
[0.6, '#4ECB73'],
[0.8, '#FBD437'],
[1, '#F47F92']
],
width: 16
}
},
pointer: {
length: '80%',
width: 3,
color: 'auto'
},
axisTick: {
show: false
},
splitLine: { show: false },
type: 'gauge',
detail: {
formatter: '{value}%',
textStyle: {
color: '#595959',
fontSize: 32
}
},
data: [{ value: 10 }]
}
]
}
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(option)
on(window, 'resize', this.resize)
})
}
}
}
</script>
<style>
.gauge-main {
width: 100%;
height: 360px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<div class="line-main" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
value: Object,
text: String,
subtext: String
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
const xAxisData = Object.keys(this.value)
const seriesData = Object.values(this.value)
const option = {
grid: {
left: '1%',
right: '1%',
top: '2%',
bottom: '1%',
containLabel: true
},
title: {
text: this.text,
subtext: this.subtext,
x: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{c}人',
// position: ['30%', '90%'],
position: 'top',
backgroundColor: '#387DE1',
textStyle: {
fontSize: 18,
color: '#fff'
}
},
xAxis: {
// show: false,
type: 'category',
data: xAxisData,
splitLine: {
show: false
}
},
yAxis: [
{
// show: false,
type: 'value',
splitLine: {
show: true,
lineStyle: {
// 设置刻度线粗度(粗的宽度)
width: 1,
// 颜色数组,数组数量要比刻度线数量大才能不循环使用
color: [
'rgba(0, 0, 0, 0)',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee',
'#eee'
]
}
}
}
],
series: [
{
data: seriesData,
type: 'line',
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#f2f5ff' },
{ offset: 1, color: '#fff' }
])
}
},
lineStyle: {
normal: {
width: 5,
color: '#36CBCB'
}
}
}
]
}
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(option)
on(window, 'resize', this.resize)
})
}
}
}
</script>
<style>
.line-main {
width: 100%;
height: 360px;
padding: 28px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<div class="pie-main" id="box" ref="dom"></div>
</template>
<script>
import echarts from 'echarts'
import tdTheme from './theme.json'
import { on, off } from './onoff'
echarts.registerTheme('tdTheme', tdTheme)
export default {
props: {
value: Array,
text: String,
subtext: String,
},
watch: {
value: {
handler: function (val, oldval) {
this.value = val
this.initChart()
},
deep: true, //对象内部的属性监听,也叫深度监听
},
},
mounted() {
this.initChart()
},
methods: {
resize() {
this.dom.resize()
},
initChart() {
this.$nextTick(() => {
const legend = this.value.map((_) => _.name)
const option = {
title: {
text: this.text,
subtext: this.subtext,
x: 'center',
},
position: {
top: 40,
},
tooltip: {
trigger: 'item',
formatter: '{c} ({d}%)',
// position: ['30%', '90%'],
position: function (point, params, dom, rect, size) {
console.log(size)
const leftWidth = size.viewSize[0] / 2 - size.contentSize[0] / 2
console.log(leftWidth)
return { left: leftWidth, bottom: 0 }
},
backgroundColor: 'transparent',
textStyle: {
fontSize: 24,
color: '#666',
},
},
legend: {
// orient: 'vertical',
top: 0,
data: legend,
backgroundColor: 'transparent',
icon: 'circle',
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['45%', '60%'],
center: ['50%', '52%'],
avoidLabelOverlap: false,
label: {
normal: {
show: false,
position: 'center',
},
emphasis: {
show: true,
textStyle: {
fontSize: '24',
},
},
},
labelLine: {
normal: {
show: false,
},
},
data: this.value,
},
],
}
this.dom = echarts.init(this.$refs.dom, 'tdTheme')
this.dom.setOption(option)
on(window, 'resize', this.resize)
})
},
},
}
</script>
<style>
.pie-main {
width: 100%;
height: 360px;
padding: 28px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,37 @@
/**
* @description 绑定事件 on(element, event, handler)
*/
export const on = (function () {
if (document.addEventListener != null) {
return function (element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function (element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
/**
* @description 解绑事件 off(element, event, handler)
*/
export const off = (function () {
if (document.removeEventListener != null) {
return function (element, event, handler) {
if (element && event) {
element.removeEventListener(event, handler, false);
}
};
} else {
return function (element, event, handler) {
if (element && event) {
element.detachEvent('on' + event, handler);
}
};
}
})();

View File

@@ -0,0 +1,490 @@
{
"color": [
"#2d8cf0",
"#19be6b",
"#ff9900",
"#E46CBB",
"#9A66E4",
"#ed3f14"
],
"backgroundColor": "rgba(0,0,0,0)",
"textStyle": {},
"title": {
"textStyle": {
"color": "#516b91"
},
"subtextStyle": {
"color": "#93b7e3"
}
},
"line": {
"itemStyle": {
"normal": {
"borderWidth": "2"
}
},
"lineStyle": {
"normal": {
"width": "2"
}
},
"symbolSize": "6",
"symbol": "emptyCircle",
"smooth": true
},
"radar": {
"itemStyle": {
"normal": {
"borderWidth": "2"
}
},
"lineStyle": {
"normal": {
"width": "2"
}
},
"symbolSize": "6",
"symbol": "emptyCircle",
"smooth": true
},
"bar": {
"itemStyle": {
"normal": {
"barBorderWidth": 0,
"barBorderColor": "#ccc"
},
"emphasis": {
"barBorderWidth": 0,
"barBorderColor": "#ccc"
}
}
},
"pie": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"scatter": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"boxplot": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"parallel": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"sankey": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"funnel": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"gauge": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"candlestick": {
"itemStyle": {
"normal": {
"color": "#edafda",
"color0": "transparent",
"borderColor": "#d680bc",
"borderColor0": "#8fd3e8",
"borderWidth": "2"
}
}
},
"graph": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"lineStyle": {
"normal": {
"width": 1,
"color": "#aaa"
}
},
"symbolSize": "6",
"symbol": "emptyCircle",
"smooth": true,
"color": [
"#2d8cf0",
"#19be6b",
"#f5ae4a",
"#9189d5",
"#56cae2",
"#cbb0e3"
],
"label": {
"normal": {
"textStyle": {
"color": "#eee"
}
}
}
},
"map": {
"itemStyle": {
"normal": {
"areaColor": "#f3f3f3",
"borderColor": "#516b91",
"borderWidth": 0.5
},
"emphasis": {
"areaColor": "rgba(165,231,240,1)",
"borderColor": "#516b91",
"borderWidth": 1
}
},
"label": {
"normal": {
"textStyle": {
"color": "#000"
}
},
"emphasis": {
"textStyle": {
"color": "rgb(81,107,145)"
}
}
}
},
"geo": {
"itemStyle": {
"normal": {
"areaColor": "#f3f3f3",
"borderColor": "#516b91",
"borderWidth": 0.5
},
"emphasis": {
"areaColor": "rgba(165,231,240,1)",
"borderColor": "#516b91",
"borderWidth": 1
}
},
"label": {
"normal": {
"textStyle": {
"color": "#000"
}
},
"emphasis": {
"textStyle": {
"color": "rgb(81,107,145)"
}
}
}
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#999999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"valueAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#999999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"logAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#999999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"timeAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#999999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"toolbox": {
"iconStyle": {
"normal": {
"borderColor": "#999"
},
"emphasis": {
"borderColor": "#666"
}
}
},
"legend": {
"textStyle": {
"color": "#999999"
}
},
"tooltip": {
"axisPointer": {
"lineStyle": {
"color": "#ccc",
"width": 1
},
"crossStyle": {
"color": "#ccc",
"width": 1
}
}
},
"timeline": {
"lineStyle": {
"color": "#8fd3e8",
"width": 1
},
"itemStyle": {
"normal": {
"color": "#8fd3e8",
"borderWidth": 1
},
"emphasis": {
"color": "#8fd3e8"
}
},
"controlStyle": {
"normal": {
"color": "#8fd3e8",
"borderColor": "#8fd3e8",
"borderWidth": 0.5
},
"emphasis": {
"color": "#8fd3e8",
"borderColor": "#8fd3e8",
"borderWidth": 0.5
}
},
"checkpointStyle": {
"color": "#8fd3e8",
"borderColor": "rgba(138,124,168,0.37)"
},
"label": {
"normal": {
"textStyle": {
"color": "#8fd3e8"
}
},
"emphasis": {
"textStyle": {
"color": "#8fd3e8"
}
}
}
},
"visualMap": {
"color": [
"#516b91",
"#59c4e6",
"#a5e7f0"
]
},
"dataZoom": {
"backgroundColor": "rgba(0,0,0,0)",
"dataBackgroundColor": "rgba(255,255,255,0.3)",
"fillerColor": "rgba(167,183,204,0.4)",
"handleColor": "#a7b7cc",
"handleSize": "100%",
"textStyle": {
"color": "#333"
}
},
"markPoint": {
"label": {
"normal": {
"textStyle": {
"color": "#eee"
}
},
"emphasis": {
"textStyle": {
"color": "#eee"
}
}
}
}
}

View File

@@ -0,0 +1,142 @@
<template>
<div class="dynamic-form">
<el-form
:model="form"
ref="dynamicForm"
:label-width="formInfo.labelWidth ? formInfo.labelWidth : '100px'"
:size="formInfo.size ? formInfo.size : 'small'"
>
<el-row v-for="fr in formInfo.formRows" :key="fr.key">
<el-col v-for="item in fr" :key="item.key" :span="item.span ? item.span : 24/fr.length">
<el-form-item
:prop="item.name"
:label="item.label"
:label-width="item.labelWidth"
:required="item.required"
:rules="item.rules"
>
<!-- input输入框 -->
<el-input
v-if="item.type === 'input'"
v-model.trim="form[item.name]"
:placeholder="item.placeholder"
:type="item.inputType"
clearable
autocomplete="new-password"
@change="item.change ? item.change(form) : ''"
></el-input>
<!-- 普通文本信息可用于不可修改字段等 -->
<span v-else-if="item.type === 'text'">{{ form[item.name] }}</span>
<!-- select选择框 -->
<!-- optionProps.label: 指定option中的label为options对象的某个属性值默认就是label字段 -->
<!-- optionProps.value: 指定option中的value为options对象的某个属性值默认就是value字段 -->
<el-select
v-else-if="item.type === 'select'"
v-model.trim="form[item.name]"
:placeholder="item.placeholder"
:filterable="item.filterable"
:remote="item.remote"
:remote-method="item.remoteMethod"
@focus="item.focus ? item.focus(form) : ''"
clearable
:disabled="item.updateDisabled && form.id != null"
style="width: 100%;"
>
<el-option
v-for="i in item.options"
:key="i.key"
:label="i[item.optionProps ? item.optionProps.label || 'label' : 'label']"
:value="i[item.optionProps ? item.optionProps.value || 'value' : 'value']"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row type="flex" justify="center">
<slot name="btns" :submitDisabled="submitDisabled" :data="form" :submit="submit">
<el-button @click="reset" size="mini"> </el-button>
<el-button type="primary" @click="submit" size="mini"> </el-button>
</slot>
</el-row>
</el-form>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
@Component({
name: 'DynamicForm'
})
export default class DynamicForm extends Vue {
@Prop()
formInfo: object
@Prop()
formData: [object,boolean]|undefined
form = {}
submitDisabled = false
@Watch('formData', { deep: true })
onRoleChange() {
if (this.formData) {
this.form = { ...this.formData }
}
}
submit() {
const dynamicForm: any = this.$refs['dynamicForm']
dynamicForm.validate((valid: boolean) => {
if (valid) {
// 提交的表单数据
const subform = { ...this.form }
const operation = this.form['id']
? this.formInfo['updateApi']
: this.formInfo['createApi']
if (operation) {
this.submitDisabled = true
operation.request(this.form).then(
(res: any) => {
this.$message.success('保存成功')
this.$emit('submitSuccess', subform)
this.submitDisabled = false
// this.cancel()
},
(e: any) => {
this.submitDisabled = false
}
)
} else {
this.$message.error('表单未设置对应的提交权限')
}
} else {
return false
}
})
}
reset() {
this.$emit('reset')
this.resetFieldsAndData()
}
/**
* 重置表单以及表单数据
*/
resetFieldsAndData() {
// 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
const df: any = this.$refs['dynamicForm']
df.resetFields()
// 重置表单数据
this.form = {}
}
mounted() {
// 组件可能还没有初始化第一次初始化的时候无法watch对象
this.form = { ...this.formData }
}
}
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div class="form-dialog">
<el-dialog :title="title" :visible="visible" :width="dialogWidth ? dialogWidth : '500px'">
<dynamic-form
ref="df"
:form-info="formInfo"
:form-data="formData"
@submitSuccess="submitSuccess"
>
<template slot="btns" slot-scope="props">
<slot name="btns">
<el-button
:disabled="props.submitDisabled"
type="primary"
@click="props.submit"
size="mini"
> </el-button>
<el-button :disabled="props.submitDisabled" @click="close()" size="mini"> </el-button>
</slot>
</template>
</dynamic-form>
</el-dialog>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import DynamicForm from './DynamicForm.vue'
@Component({
name: 'DynamicFormDialog',
components: {
DynamicForm
}
})
export default class DynamicFormDialog extends Vue {
@Prop()
visible: boolean|undefined
@Prop()
dialogWidth: string|undefined
@Prop()
title: string|undefined
@Prop()
formInfo: object|undefined
@Prop()
formData: [object,boolean]|undefined
close() {
// 更新父组件visible prop对应的值为false
this.$emit('update:visible', false)
// 关闭窗口则将表单数据置为null
this.$emit('update:formData', null)
this.$emit('close')
// 取消动态表单的校验以及form数据
setTimeout(() => {
const df: any = this.$refs.df
df.resetFieldsAndData()
}, 200)
}
submitSuccess(form: any) {
this.$emit('submitSuccess', form)
this.close()
}
}
</script>

View File

@@ -0,0 +1,2 @@
export { default as DynamicForm } from './DynamicForm.vue';
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';

View File

@@ -0,0 +1,337 @@
<template>
<div class="main">
<div class="header">
<div class="logo">
<span class="big">Mayfly-Go</span>
</div>
<div class="right">
<span class="header-btn">
<el-badge :value="3" class="badge">
<i class="el-icon-bell"></i>
</el-badge>
</span>
<el-dropdown>
<span class="header-btn">
{{username}}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="this.$router.push('/personal')">
<i style="padding-right: 8px" class="fa fa-cog"></i>个人中心
</el-dropdown-item>
<el-dropdown-item @click.native="logout">
<i style="padding-right: 8px" class="fa fa-key"></i>退出系统
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="app">
<div class="aside">
<div class="menu">
<el-menu
background-color="#222d32"
text-color="#bbbbbb"
active-text-color="#fff"
class="menu"
>
<MenuTree @toPath="toPath" :menus="this.menus"></MenuTree>
</el-menu>
</div>
</div>
<div class="app-body">
<el-tabs
id="nav-bar"
class="none-select"
v-model="activeName"
@tab-click="tabClick"
@tab-remove="removeTab"
type="card"
closable
>
<el-tab-pane
:key="item.name"
v-for="(item) in tabs"
:label="item.title"
:name="item.name"
></el-tab-pane>
</el-tabs>
<div id="mainContainer" class="main-container">
<router-view v-if="!iframe"></router-view>
<iframe
style="width: calc(100% - 235px); height: calc(100% - 90px)"
rameborder="0"
v-else
:src="iframeSrc"
></iframe>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import MenuTree from './MenuTree.vue'
import api from '@/common/openApi'
import { AuthUtils } from '../common/AuthUtils'
@Component({
name: 'Layout',
components: {
MenuTree,
},
})
export default class App extends Vue {
private iframe = false
private iframeSrc: string | null = null
private username = ''
private menus: Array<object> = []
private tabs: Array<any> = []
private activeName = ''
private tabIndex = 2
private toPath(menu: any) {
const path = menu.url
this.goToPath(path)
this.addTab(path, menu.name)
}
private goToPath(path: string) {
// 如果是请求其他地址则使用iframe展示
if (path && (path.startsWith('http://') || path.startsWith('https://'))) {
this.iframe = true
this.iframeSrc = path
return
}
this.iframe = false
this.iframeSrc = null
this.$router
.push({
path,
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch((err: any) => {})
}
private tabClick(tab: any) {
this.goToPath(tab.name)
}
private addTab(path: string, title: string) {
for (const n of this.tabs) {
if (n.name === path) {
this.activeName = path
return
}
}
this.tabs.push({
name: path,
title: title,
})
this.activeName = path
}
private removeTab(targetName: string) {
const tabs = this.tabs
let activeName = this.activeName
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name == targetName) {
const nextTab = tabs[index + 1] || tabs[index - 1]
if (nextTab) {
activeName = nextTab.name
}
}
})
}
this.activeName = activeName
this.tabs = tabs.filter((tab) => tab.name !== targetName)
this.goToPath(activeName)
}
private async logout() {
sessionStorage.clear()
this.$router.push({
path: '/login',
})
}
mounted() {
const menu = [
{
id: 1,
type: 1,
name: '机器管理',
icon: 'el-icon-menu',
children: [
{
type: 1,
name: '机器列表',
url: '/machines',
icon: 'el-icon-menu',
code: 'index',
},
],
},
]
if (menu != null) {
this.menus = menu
}
const user = sessionStorage.getItem('admin')
if (user != null) {
this.username = JSON.parse(user).username
}
this.addTab(this.$route.path, this.$route.meta.title)
}
}
</script>>
<style lang="less">
.main {
display: flex;
.el-menu:not(.el-menu--collapse) {
width: 230px;
}
.app {
width: 100%;
background-color: #ecf0f5;
}
.aside {
position: fixed;
margin-top: 50px;
z-index: 10;
background-color: #222d32;
transition: all 0.3s ease-in-out;
.menu {
overflow-y: auto;
height: calc(~'100vh');
}
}
.app-body {
margin-left: 230px;
-webkit-transition: margin-left 0.3s ease-in-out;
transition: margin-left 0.3s ease-in-out;
}
.main-container {
margin-top: 88px;
padding: 2px;
min-height: calc(~'100vh - 88px');
}
}
.header {
width: 100%;
position: fixed;
display: flex;
height: 50px;
background-color: #303643;
z-index: 10;
.logo {
.min {
display: none;
}
width: 230px;
height: 50px;
text-align: center;
line-height: 50px;
color: #fff;
background-color: #303643;
-webkit-transition: width 0.35s;
transition: all 0.3s ease-in-out;
}
.right {
position: absolute;
right: 0;
}
.header-btn {
.el-badge__content {
top: 14px;
right: 7px;
text-align: center;
font-size: 9px;
padding: 0 3px;
background-color: #00a65a;
color: #fff;
border: none;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25em;
}
overflow: hidden;
height: 50px;
display: inline-block;
text-align: center;
line-height: 50px;
cursor: pointer;
padding: 0 14px;
color: #fff;
&:hover {
background-color: #222d32;
}
}
}
.menu {
border-right: none;
// 禁止选择
moz-user-select: -moz-none;
-moz-user-select: none;
-o-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.el-menu--vertical {
min-width: 190px;
}
.setting-category {
padding: 10px 0;
border-bottom: 1px solid #eee;
}
#mainContainer iframe {
border: none;
outline: none;
width: 100%;
height: 100%;
position: absolute;
background-color: #ecf0f5;
}
.el-submenu__title {
font-weight: 500;
}
.el-menu-item {
font-weight: 500;
}
#nav-bar {
margin-top: 50px;
height: 38px;
width: 100%;
z-index: 8;
background: #fff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
position: fixed;
top: 0;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div>
<template v-for="menu in this.menus">
<!-- 只有菜单的子节点为菜单类型才继续展开 -->
<el-submenu
:key="menu.id"
:index="!menu.code ? menu.id + '' : menu.code"
v-if="menu.children && menu.children[0].type === 1"
>
<template slot="title">
<i :class="menu.icon"></i>
<span slot="title">{{menu.name}}</span>
</template>
<MenuTree @toPath="toPath" :menus="menu.children"></MenuTree>
</el-submenu>
<el-menu-item
@click="toPath(menu)"
:key="menu.id"
:index="!menu.path ? menu.id + '' : menu.path"
v-else
>
<i class="iconfont" :class="menu.icon"></i>
<span slot="title">{{menu.name}}</span>
</el-menu-item>
</template>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component({
name: 'MenuTree'
})
export default class MenuTree extends Vue {
@Prop()
menus: object
toPath(menu: any) {
this.$emit('toPath', menu)
}
}
</script>>

View File

@@ -0,0 +1,25 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './assets/css/style.css'
// import ECharts from 'vue-echarts' // 在 webpack 环境下指向 components/ECharts.vue
// 手动引入 ECharts 各模块来减小打包体积
// import 'echarts/lib/chart/bar'
// import 'echarts/lib/component/tooltip'
Vue.config.productionTip = false
// 注册组件后即可使用
// Vue.component('v-chart', ECharts)
Vue.use(ElementUI)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

View File

@@ -0,0 +1,57 @@
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from "@/layout/Layout.vue"
import { AuthUtils } from '../common/AuthUtils';
Vue.use(VueRouter)
const routes: Array<RouteConfig> = [
{
path: '/login',
name: 'Login',
meta: {
title: '登录',
keepAlive: false
},
component: () => import('@/views/login/Login.vue')
},
{
path: '/',
component: Layout,
meta: {
title: '首页',
keepAlive: false,
},
children: [{
path: 'machines',
name: 'machines',
meta: {
title: '机器列表',
keepAlive: false
},
component: () => import('@/views/machine')
}]
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to: any, from: any, next: any) => {
window.document.title = to.meta.title
const toPath = to.path
if (toPath.startsWith('/open')) {
next()
return
}
if (!AuthUtils.getToken() && toPath != '/login') {
next({ path: '/login' });
} else {
next();
}
});
export default router

13
mayfly-go-front/src/shims-tsx.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
mayfly-go-front/src/shims-vue.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

View File

@@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

View File

@@ -0,0 +1,28 @@
.login{
display: flex;
justify-content: center;
align-items: center;
position: absolute;
height: 100%;
width: 100%;
background-color: #e4e5e6;
.login-form{
width: 375px;
height: 435px;
padding: 30px;
background-color: white;
text-align: left;
border-radius: 4px;
position: relative;
margin-left: 0;
margin-right: 0;
zoom: 1;
display: block;
.login-header{
text-align: center;
font-size: 16px;
font-weight: bold;
margin-bottom: 20px;
}
}
}

View File

@@ -0,0 +1,145 @@
<template>
<div class="login">
<div class="login-form">
<div class="login-header">
<img src="../../assets/images/logo.png" width="150" height="120" alt />
<!-- <p>{{ $Config.name.siteName }}</p> -->
</div>
<el-input
placeholder="请输入用户名"
suffix-icon="fa fa-user"
v-model="loginForm.username"
style="margin-bottom: 18px"
></el-input>
<el-input
placeholder="请输入密码"
suffix-icon="fa fa-keyboard-o"
v-model="loginForm.password"
type="password"
style="margin-bottom: 18px"
autocomplete="new-password"
></el-input>
<!-- <el-row>
<el-col :span="12">
<img
@click="getCaptcha"
width="130px"
height="40px"
:src="captchaImage"
style="cursor: pointer"
/>
</el-col>
<el-col :span="12">
<el-input
placeholder="请输入算术结果"
suffix-icon="fa fa-user"
v-model="loginForm.captcha"
style="margin-bottom: 18px"
@keyup.native.enter="login"
></el-input>
</el-col>
</el-row> -->
<el-button
type="primary"
:loading="loginLoading"
style="width: 100%;margin-bottom: 18px"
@click.native="login"
>登录</el-button>
<div>
<el-checkbox v-model="remember">记住密码</el-checkbox>
<!-- <a href="javascript:;" style="float: right;color: #3C8DBC;font-size: 14px">Register</a> -->
</div>
</div>
</div>
</template>
<script lang="ts">
import openApi from '../../common/openApi'
import { Component, Vue } from 'vue-property-decorator'
import { AuthUtils } from '@/common/AuthUtils'
@Component({
name: 'Login',
})
export default class Login extends Vue {
// private captchaImage = ''
private loginForm = {
username: '',
password: '',
// captcha: '',
uuid: '',
}
private remember = false
private loginLoading = false
mounted() {
// this.getCaptcha()
const r = this.getRemember()
let rememberAccount: any
if (r != null) {
rememberAccount = JSON.parse(r)
}
if (rememberAccount) {
this.remember = true
this.loginForm.username = rememberAccount.username
this.loginForm.password = rememberAccount.password
} else {
this.remember = false
}
}
private async getCaptcha() {
const res: any = await openApi.captcha()
// this.captchaImage = res.base64Img
this.loginForm.uuid = res.uuid
}
private async login() {
this.loginLoading = true
try {
const res = await openApi.login(this.loginForm)
if (this.remember) {
localStorage.setItem('remember', JSON.stringify(this.loginForm))
} else {
localStorage.removeItem('remember')
}
setTimeout(() => {
//保存用户token以及菜单按钮权限
// this['$Permission'].savePermission(res)
AuthUtils.saveToken(res.token)
this.$notify({
title: '登录成功',
message: '很高兴你使用Mayfly Admin别忘了给个Star哦。',
type: 'success',
})
this.loginLoading = false
// 有重定向则重定向,否则到首页
const redirect: any = this.$route.query.redirect
if (redirect) {
this.$router.push(redirect)
} else {
this.$router.push({
path: '/',
})
}
}, 500)
} catch (err) {
this.loginLoading = false
// this.loginForm.captcha = ''
// this.getCaptcha()
}
}
private getRemember() {
return localStorage.getItem('remember')
}
}
</script>
<style lang="less">
@import 'Login.less';
</style>

View File

@@ -0,0 +1,333 @@
<template>
<div>
<div class="toolbar">
<div class="fl">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="openFormDialog(false)"
plain
>添加</el-button>
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
:disabled="currentId == null"
@click="openFormDialog(currentData)"
plain
>编辑</el-button>
<el-button
:disabled="currentId == null"
@click="deleteMachine(currentId)"
type="danger"
icon="el-icon-delete"
size="mini"
>删除</el-button>
<el-button
type="success"
:disabled="currentId == null"
@click="fileManage(currentData)"
size="mini"
plain
>文件管理</el-button>
</div>
<div style="float: right;">
<el-input
placeholder="host"
size="mini"
style="width: 140px;"
v-model="params.host"
@clear="search"
plain
clearable
></el-input>
<el-button @click="search" type="success" icon="el-icon-search" size="mini"></el-button>
</div>
</div>
<el-table :data="data.list" stripe style="width: 100%" @current-change="choose">
<el-table-column label="选择" width="55px">
<template slot-scope="scope">
<el-radio v-model="currentId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width></el-table-column>
<el-table-column prop="ip" label="IP" width></el-table-column>
<el-table-column prop="port" label="端口" width></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="updateTime" label="更新时间"></el-table-column>
<el-table-column label="操作" min-width="200px">
<template slot-scope="scope">
<el-button
type="primary"
@click="info(scope.row.id)"
:ref="scope.row"
icom="el-icon-tickets"
size="mini"
plain
>基本信息</el-button>
<el-button
type="primary"
@click="monitor(scope.row.id)"
:ref="scope.row"
icom="el-icon-tickets"
size="mini"
plain
>监控</el-button>
<el-button
type="success"
@click="serviceManager(scope.row)"
:ref="scope.row"
size="mini"
plain
>服务管理</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="data.total"
:current-page.sync="params.pageNum"
:page-size="params.pageSize"
/>
<el-dialog title="基本信息" :visible.sync="infoDialog.visible" width="30%">
<div style="white-space: pre-line;">{{infoDialog.info}}</div>
<!-- <span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="dialogVisible = false"> </el-button>
</span>-->
</el-dialog>
<el-dialog @close="closeMonitor" title="监控信息" :visible.sync="monitorDialog.visible" width="60%">
<monitor ref="monitorDialog" :machineId="monitorDialog.machineId" />
</el-dialog>
<!-- <FileManage
:title="dialog.title"
:visible.sync="dialog.visible"
:machineId.sync="dialog.machineId"
/>-->
<dynamic-form-dialog
:visible.sync="formDialog.visible"
:title="formDialog.title"
:formInfo="formDialog.formInfo"
:formData.sync="formDialog.formData"
@submitSuccess="submitSuccess"
></dynamic-form-dialog>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { DynamicFormDialog } from '@/components/dynamic-form'
import Monitor from './Monitor.vue'
import { machineApi } from './api'
@Component({
name: 'MachineList',
components: {
DynamicFormDialog,
Monitor,
},
})
export default class MachineList extends Vue {
data = {
list: [],
total: 10,
}
infoDialog = {
visible: false,
info: '',
}
monitorDialog = {
visible: false,
machineId: 0,
}
currentId = null
currentData: any = null
params = {
pageNum: 1,
pageSize: 10,
host: null,
clusterId: null,
}
dialog = {
machineId: null,
visible: false,
title: '',
}
formDialog = {
visible: false,
title: '',
formInfo: {
createApi: machineApi.save,
updateApi: machineApi.update,
formRows: [
[
{
type: 'input',
label: '名称:',
name: 'name',
placeholder: '请输入名称',
rules: [
{
required: true,
message: '请输入名称',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: 'ip',
name: 'ip',
placeholder: '请输入ip',
rules: [
{
required: true,
message: '请输入ip',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '端口号:',
name: 'port',
placeholder: '请输入端口号',
inputType: 'number',
rules: [
{
required: true,
message: '请输入ip',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '用户名:',
name: 'username',
placeholder: '请输入用户名',
rules: [
{
required: true,
message: '请输入用户名',
trigger: ['blur', 'change'],
},
],
},
],
[
{
type: 'input',
label: '密码:',
name: 'password',
placeholder: '请输入密码',
inputType: 'password',
},
],
],
},
formData: { port: 22 },
}
mounted() {
this.search()
}
choose(item: any) {
if (!item) {
return
}
this.currentId = item.id
this.currentData = item
}
async info(id: number) {
const res = await machineApi.info.request({ id })
this.infoDialog.info = res
this.infoDialog.visible = true
// res.data
// this.$alert(res, '机器基本信息', {
// type: 'info',
// dangerouslyUseHTMLString: false,
// closeOnClickModal: true,
// showConfirmButton: false,
// }).catch((r) => {
// console.log(r)
// })
}
monitor(id: number) {
this.monitorDialog.machineId = id
this.monitorDialog.visible = true
// 如果重复打开同一个则开启定时任务
const md: any = this.$refs['monitorDialog']
if (md) {
md.startInterval()
}
}
closeMonitor() {
// 关闭窗口,取消定时任务
const md: any = this.$refs['monitorDialog']
md.cancelInterval()
}
openFormDialog(redis: any) {
let dialogTitle
if (redis) {
this.formDialog.formData = this.currentData
dialogTitle = '编辑机器'
} else {
this.formDialog.formData = { port: 22 }
dialogTitle = '添加机器'
}
this.formDialog.title = dialogTitle
this.formDialog.visible = true
}
async deleteMachine(id: number) {
await machineApi.del.request({ id })
this.$message.success('操作成功')
this.search()
}
fileManage(row: any) {
this.dialog.machineId = row.id
this.dialog.visible = true
this.dialog.title = `${row.name} => ${row.ip}`
}
submitSuccess() {
this.currentId = null
;(this.currentData = null), this.search()
}
async search() {
const res = await machineApi.list.request(this.params)
this.data = res
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,325 @@
<template>
<div>
<el-row>
<el-col>
<HomeCard desc="Base info" title="基础信息">
<ActivePlate :infoList="infoCardData" />
</HomeCard>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :lg="6" :md="24">
<HomeCard desc="Task info" title="任务">
<ChartPie :value.sync="taskData" />
</HomeCard>
</el-col>
<el-col :lg="6" :md="24">
<HomeCard desc="Mem info" title="内存">
<ChartPie :value.sync="memData" />
</HomeCard>
</el-col>
<el-col :lg="6" :md="24">
<HomeCard desc="Swap info" title="CPU">
<ChartPie :value.sync="cpuData" />
</HomeCard>
</el-col>
</el-row>
<!-- <el-row :gutter="20">
<el-col :lg="18" :md="24">
<HomeCard desc="User active" title="每周用户活跃量">
<ChartLine :value="lineData" />
</HomeCard>
</el-col>
</el-row>-->
<el-row :gutter="20">
<el-col :lg="12" :md="24">
<ChartContinuou :value="this.data" title="内存" />
</el-col>
<el-col :lg="12" :md="24">
<ChartContinuou :value="this.data" title="CPU" />
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :lg="12" :md="24">
<HomeCard desc="load info" title="负载情况">
<BaseChart :option="this.loadChartOption" />
</HomeCard>
</el-col>
<el-col :lg="12" :md="24">
<ChartContinuou :value="this.data" title="磁盘IO" />
</el-col>
</el-row>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
import ActivePlate from '@/components/chart/ActivePlate.vue'
import HomeCard from '@/components/chart/Card.vue'
import ChartPie from '@/components/chart/ChartPie.vue'
import ChartLine from '@/components/chart/ChartLine.vue'
import ChartGauge from '@/components/chart/ChartGauge.vue'
import ChartBar from '@/components/chart/ChartBar.vue'
import ChartFunnel from '@/components/chart/ChartFunnel.vue'
import ChartContinuou from '@/components/chart/ChartContinuou.vue'
import BaseChart from '@/components/chart/BaseChart.vue'
import { machineApi } from './api'
@Component({
name: 'Monitor',
components: {
HomeCard,
ActivePlate,
ChartPie,
ChartFunnel,
ChartLine,
ChartGauge,
ChartBar,
ChartContinuou,
BaseChart,
},
})
export default class Monitor extends Vue {
@Prop()
machineId: number
timer: number
infoCardData = [
{
title: 'total task',
icon: 'md-person-add',
count: 0,
color: '#11A0F8',
},
{ title: '总内存', icon: 'md-locate', count: '', color: '#FFBB44 ' },
{
title: '可用内存',
icon: 'md-help-circle',
count: '',
color: '#7ACE4C',
},
{ title: '空闲交换空间', icon: 'md-share', count: 657, color: '#11A0F8' },
{
title: '使用中交换空间',
icon: 'md-chatbubbles',
count: 12,
color: '#91AFC8',
},
{ title: '新增页面', icon: 'md-map', count: 14, color: '#91AFC8' },
]
taskData = [
{ value: 0, name: '运行中', color: '#3AA1FFB' },
{ value: 0, name: '睡眠中', color: '#36CBCB' },
{ value: 0, name: '结束', color: '#4ECB73' },
{ value: 0, name: '僵尸', color: '#F47F92' },
]
memData = [
{ value: 0, name: '空闲', color: '#3AA1FFB' },
{ value: 0, name: '使用中', color: '#36CBCB' },
{ value: 0, name: '缓存', color: '#4ECB73' },
]
swapData = [
{ value: 0, name: '空闲', color: '#3AA1FFB' },
{ value: 0, name: '使用中', color: '#36CBCB' },
]
cpuData = [
{ value: 0, name: '用户空间', color: '#3AA1FFB' },
{ value: 0, name: '内核空间', color: '#36CBCB' },
{ value: 0, name: '改变优先级', color: '#4ECB73' },
{ value: 0, name: '空闲率', color: '#4ECB73' },
{ value: 0, name: '等待IO', color: '#4ECB73' },
{ value: 0, name: '硬中断', color: '#4ECB73' },
{ value: 0, name: '软中断', color: '#4ECB73' },
{ value: 0, name: '虚拟机', color: '#4ECB73' },
]
data = [
['06/05 15:01', 116.12],
['06/05 15:06', 129.21],
['06/05 15:11', 135.43],
['2000-06-08', 86.33],
['2000-06-09', 73.98],
['2000-06-10', 85],
['2000-06-11', 73],
['2000-06-12', 68],
['2000-06-13', 92],
['2000-06-14', 130],
['2000-06-15', 245],
['2000-06-16', 139],
['2000-06-17', 115],
['2000-06-18', 111],
['2000-06-19', 309],
['2000-06-20', 206],
['2000-06-21', 137],
['2000-06-22', 128],
['2000-06-23', 85],
['2000-06-24', 94],
['2000-06-25', 71],
['2000-06-26', 106],
['2000-06-27', 84],
['2000-06-28', 93],
['2000-06-29', 85],
['2000-06-30', 73],
['2000-07-01', 83],
['2000-07-02', 125],
['2000-07-03', 107],
['2000-07-04', 82],
['2000-07-05', 44],
['2000-07-06', 72],
['2000-07-07', 106],
['2000-07-08', 107],
['2000-07-09', 66],
['2000-07-10', 91],
['2000-07-11', 92],
['2000-07-12', 113],
['2000-07-13', 107],
['2000-07-14', 131],
['2000-07-15', 111],
['2000-07-16', 64],
['2000-07-17', 69],
['2000-07-18', 88],
['2000-07-19', 77],
['2000-07-20', 83],
['2000-07-21', 111],
['2000-07-22', 57],
['2000-07-23', 55],
['2000-07-24', 60],
]
dateList = this.data.map(function (item) {
return item[0]
})
valueList = this.data.map(function (item) {
return item[1]
})
loadChartOption = {
// Make gradient line here
visualMap: [
{
show: false,
type: 'continuous',
seriesIndex: 0,
min: 0,
max: 400,
},
],
legend: {
data: ['1分钟', '5分钟', '15分钟'],
},
tooltip: {
trigger: 'axis',
},
xAxis: [
{
data: this.dateList,
},
],
yAxis: [
{
splitLine: { show: false },
},
],
grid: [{}],
series: [
{
name: '1分钟',
type: 'line',
showSymbol: false,
data: this.valueList,
},
{
name: '5分钟',
type: 'line',
showSymbol: false,
data: [100, 22, 33, 121, 32, 332, 322, 222, 232],
},
{
name: '15分钟',
type: 'line',
showSymbol: true,
data: [130, 222, 373, 135, 456, 332, 333, 343, 342],
},
],
}
lineData = {
Mon: 13253,
Tue: 34235,
Wed: 26321,
Thu: 12340,
Fri: 24643,
Sat: 1322,
Sun: 1324,
}
@Watch('machineId', { deep: true })
onDataChange() {
if (this.machineId) {
this.intervalGetTop()
}
}
mounted() {
this.intervalGetTop()
}
beforeDestroy() {
this.cancelInterval()
}
cancelInterval() {
clearInterval(this.timer)
this.timer = 0
}
startInterval() {
if (!this.timer) {
this.timer = setInterval(this.getTop, 3000)
}
}
intervalGetTop() {
this.getTop()
this.startInterval()
}
async getTop() {
const topInfo = await machineApi.top.request({ id: this.machineId })
this.infoCardData[0].count = topInfo.totalTask
this.infoCardData[1].count = Math.round(topInfo.totalMem / 1024) + 'M'
this.infoCardData[2].count = Math.round(topInfo.availMem / 1024) + 'M'
this.infoCardData[3].count = Math.round(topInfo.freeSwap / 1024) + 'M'
this.infoCardData[4].count = Math.round(topInfo.usedSwap / 1024) + 'M'
this.taskData[0].value = topInfo.runningTask
this.taskData[1].value = topInfo.sleepingTask
this.taskData[2].value = topInfo.stoppedTask
this.taskData[3].value = topInfo.zombieTask
this.memData[0].value = Math.round(topInfo.freeMem / 1024)
this.memData[1].value = Math.round(topInfo.usedMem / 1024)
this.memData[2].value = Math.round(topInfo.cacheMem / 1024)
this.cpuData[0].value = topInfo.cpuUs
this.cpuData[1].value = topInfo.cpuSy
this.cpuData[2].value = topInfo.cpuNi
this.cpuData[3].value = topInfo.cpuId
this.cpuData[4].value = topInfo.cpuWa
this.cpuData[5].value = topInfo.cpuHi
this.cpuData[6].value = topInfo.cpuSi
this.cpuData[7].value = topInfo.cpuSt
}
}
</script>
<style lang="less">
.count-style {
font-size: 50px;
}
</style>

View File

@@ -0,0 +1,25 @@
import Api from '@/common/Api';
export const machineApi = {
// 获取权限列表
list: Api.create("/machines", 'get'),
info: Api.create("/machines/{id}/sysinfo", 'get'),
top: Api.create("/machines/{id}/top", 'get'),
// 保存按钮
save: Api.create("/devops/machines", 'post'),
update: Api.create("/devops/machines/{id}", 'put'),
// 删除机器
del: Api.create("/devops/machines/{id}", 'delete'),
// 获取配置文件列表
files: Api.create("/devops/machines/{id}/files", 'get'),
lsFile: Api.create("/devops/machines/files/{fileId}/ls", 'get'),
rmFile: Api.create("/devops/machines/files/{fileId}/rm", 'delete'),
uploadFile: Api.create("/devops/machines/files/upload", 'post'),
fileContent: Api.create("/devops/machines/files/{fileId}/cat", 'get'),
// 修改文件内容
updateFileContent: Api.create("/devops/machines/files/{id}", 'put'),
// 添加文件or目录
addConf: Api.create("/devops/machines/{machineId}/files", 'post'),
// 删除配置的文件or目录
delConf: Api.create("/devops/machines/files/{id}", 'delete'),
}

View File

@@ -0,0 +1 @@
export { default } from './MachineList.vue';

View File

@@ -0,0 +1,45 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
// 定义一个变量就必须给它一个初始值
"strictPropertyInitialization": false,
"suppressImplicitAnyIndexErrors": true,
// 允许编译javascript文件
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,80 @@
const merge = require("webpack-merge");
const tsImportPluginFactory = require("ts-import-plugin");
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following method:
// port = 8000 npm run dev OR npm run dev --port = 8000
const port = process.env.port || process.env.npm_config_port || 8000 // dev port
module.exports = {
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: false,
productionSourceMap: false,
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name: 'eatlife',
resolve: {
alias: {
'@': resolve('src')
}
}
},
transpileDependencies: [
'vue-echarts',
'resize-detector'
],
chainWebpack: config => {
config.module
.rule("ts")
.use("ts-loader")
.tap(options => {
options = merge(options, {
transpileOnly: true,
getCustomTransformers: () => ({
before: [
tsImportPluginFactory({
libraryName: "vant",
libraryDirectory: "es",
style: true
})
]
}),
compilerOptions: {
module: "es2015"
}
});
return options;
});
// 自动注入通用的scss不需要自己在每个文件里手动注入
// const types = ['vue-modules', 'vue', 'normal-modules', 'normal']
// types.forEach(type => {
// config.module.rule('scss').oneOf(type)
// .use('sass-resource')
// .loader('sass-resources-loader')
// .options({
// resources: [
// path.resolve(__dirname, './src/assets/styles/global.scss'),
// ],
// });
// });
}
};

25
models/account.go Normal file
View File

@@ -0,0 +1,25 @@
package models
import (
"github.com/astaxie/beego/orm"
"mayfly-go/base"
"mayfly-go/controllers/vo"
)
type Account struct {
base.Model
Username string `orm:"column(username)" json:"username"`
Password string `orm:"column(password)" json:"-"`
Status int8 `json:"status"`
}
func init() {
orm.RegisterModelWithPrefix("t_", new(Account))
}
func ListAccount(param *base.PageParam, args ...interface{}) base.PageResult {
sql := "SELECT a.id, a.username, a.create_time, a.creator_id, a.creator, r.Id AS 'Role.Id', r.Name AS 'Role.Name'" +
" FROM t_account a LEFT JOIN t_role r ON a.id = r.account_id"
return base.GetPageBySql(sql, new([]vo.AccountVO), param, args)
}

45
models/machine.go Normal file
View File

@@ -0,0 +1,45 @@
package models
import (
"github.com/astaxie/beego/orm"
"mayfly-go/base"
"mayfly-go/controllers/vo"
)
type Machine struct {
base.Model
Name string `orm:"column(name)"`
// IP地址
Ip string `orm:"column(ip)" json:"ip"`
// 用户名
Username string `orm:"column(username)" json:"username"`
Password string `orm:"column(password)" json:"-"`
// 端口号
Port int `orm:"column(port)" json:"port"`
}
func init() {
orm.RegisterModelWithPrefix("t_", new(Machine))
}
func GetMachineById(id uint64) *Machine {
machine := new(Machine)
machine.Id = id
err := base.GetBy(machine)
if err != nil {
return nil
}
return machine
}
// 分页获取机器信息列表
func GetMachineList(pageParam *base.PageParam) base.PageResult {
m := new([]Machine)
querySetter := base.QuerySetter(new(Machine)).OrderBy("-Id")
return base.GetPage(querySetter, pageParam, m, new([]vo.MachineVO))
}
// 获取所有需要监控的机器信息列表
func GetNeedMonitorMachine() *[]orm.Params {
return base.GetListBySql("SELECT id FROM t_machine WHERE need_monitor = 1")
}

19
models/machine_monitor.go Normal file
View File

@@ -0,0 +1,19 @@
package models
import (
"github.com/astaxie/beego/orm"
"time"
)
type MachineMonitor struct {
Id uint64 `orm:"column(id)" json:"id"`
MachineId uint64 `orm:"column(machine_id)" json:"machineId"`
CpuRate float32 `orm:"column(cpu_rate)" json:"cpuRate"`
MemRate float32 `orm:"column(mem_rate)" json:"memRate"`
SysLoad string `orm:"column(sys_load)" json:"sysLoad"`
CreateTime time.Time `orm:"column(create_time)" json:"createTime"`
}
func init() {
orm.RegisterModelWithPrefix("t_", new(MachineMonitor))
}

19
models/role.go Normal file
View File

@@ -0,0 +1,19 @@
package models
import (
"github.com/astaxie/beego/orm"
"mayfly-go/base"
)
type Role struct {
base.Model
Name string `orm:"column(name)" json:"username"`
//AccountId int64 `orm:"column(account_id)`
Account *Account `orm:"rel(fk);index"`
}
func init() {
orm.RegisterModelWithPrefix("t_", new(Role))
}

23
routers/router.go Normal file
View File

@@ -0,0 +1,23 @@
package routers
import (
"github.com/astaxie/beego"
"mayfly-go/controllers"
)
func init() {
//beego.Router("/account/login", &controllers.LoginController{})
//beego.Router("/account", &controllers.AccountController{})
//beego.Include(&controllers.AccountController{})
//beego.Include()
beego.Router("/api/accounts/login", &controllers.AccountController{}, "post:Login")
beego.Router("/api/accounts", &controllers.AccountController{}, "get:Accounts")
machine := &controllers.MachineController{}
beego.Router("/api/machines", machine, "get:Machines")
beego.Router("/api/machines/?:machineId/run", machine, "get:Run")
beego.Router("/api/machines/?:machineId/top", machine, "get:Top")
beego.Router("/api/machines/?:machineId/sysinfo", machine, "get:SysInfo")
beego.Router("/api/machines/?:machineId/process", machine, "get:GetProcessByName")
//beego.Router("/machines/?:machineId/ws", machine, "get:WsSSH")
}

30
scheudler/mytask.go Normal file
View File

@@ -0,0 +1,30 @@
package scheduler
import (
"github.com/siddontang/go/log"
"mayfly-go/base"
"mayfly-go/base/utils"
"mayfly-go/machine"
"mayfly-go/models"
)
func init() {
SaveMachineMonitor()
}
func SaveMachineMonitor() {
AddFun("@every 60s", func() {
for _, m := range *models.GetNeedMonitorMachine() {
m := m
go func() {
mm := machine.GetMonitorInfo(machine.GetCli(uint64(utils.GetInt4Map(m, "id"))))
if mm != nil {
err := base.Insert(mm)
if err != nil {
log.Error("保存机器监控信息失败: %s", err.Error())
}
}
}()
}
})
}

28
scheudler/scheduler.go Normal file
View File

@@ -0,0 +1,28 @@
package scheduler
import (
"github.com/robfig/cron/v3"
"mayfly-go/base"
)
var c = cron.New()
func Start() {
c.Start()
}
func Stop() {
c.Stop()
}
func GetCron() *cron.Cron {
return c
}
func AddFun(spec string, cmd func()) cron.EntryID {
id, err := c.AddFunc(spec, cmd)
if err != nil {
panic(base.NewBizErr("添加任务失败:" + err.Error()))
}
return id
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
static/index.html Normal file
View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>mayfly-go-front</title><link href=/static/css/chunk-6c422708.2d81c5bb.css rel=prefetch><link href=/static/js/chunk-4a3c1aef.94cc2a02.js rel=prefetch><link href=/static/js/chunk-6c422708.a09466dd.js rel=prefetch><link href=/static/js/chunk-945da412.570aca5d.js rel=prefetch><link href=/static/css/app.e8323368.css rel=preload as=style><link href=/static/css/chunk-vendors.08810481.css rel=preload as=style><link href=/static/js/app.aa8651f8.js rel=preload as=script><link href=/static/js/chunk-vendors.a6a99ea9.js rel=preload as=script><link href=/static/css/chunk-vendors.08810481.css rel=stylesheet><link href=/static/css/app.e8323368.css rel=stylesheet></head><body><noscript><strong>We're sorry but mayfly-go-front doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/static/js/chunk-vendors.a6a99ea9.js></script><script src=/static/js/app.aa8651f8.js></script></body></html>

View File

@@ -0,0 +1 @@
#app{background-color:#222d32}.main{display:flex}.main .el-menu:not(.el-menu--collapse){width:230px}.main .app{width:100%;background-color:#ecf0f5}.main .aside{position:fixed;margin-top:50px;z-index:10;background-color:#222d32;transition:all .3s ease-in-out}.main .aside .menu{overflow-y:auto;height:100vh}.main .app-body{margin-left:230px;transition:margin-left .3s ease-in-out}.main .main-container{margin-top:88px;padding:2px;min-height:calc(100vh - 88px)}.header{width:100%;position:fixed;display:flex;z-index:10}.header,.header .logo{height:50px;background-color:#303643}.header .logo{width:230px;text-align:center;line-height:50px;color:#fff;transition:all .3s ease-in-out}.header .logo .min{display:none}.header .right{position:absolute;right:0}.header .header-btn{overflow:hidden;height:50px;display:inline-block;text-align:center;line-height:50px;cursor:pointer;padding:0 14px;color:#fff}.header .header-btn .el-badge__content{top:14px;right:7px;text-align:center;font-size:9px;padding:0 3px;background-color:#00a65a;color:#fff;border:none;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.header .header-btn:hover{background-color:#222d32}.menu{border-right:none;moz-user-select:-moz-none;-moz-user-select:none;-o-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.el-menu--vertical{min-width:190px}.setting-category{padding:10px 0;border-bottom:1px solid #eee}#mainContainer iframe{border:none;outline:none;width:100%;height:100%;position:absolute;background-color:#ecf0f5}.el-menu-item,.el-submenu__title{font-weight:500}#nav-bar{margin-top:50px;height:38px;width:100%;z-index:8;background:#fff;box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04);position:fixed;top:0}*{padding:0;margin:0;outline:none;box-sizing:border-box}body{font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,,Arial,sans-serif}a{color:#3c8dbc;text-decoration:none}::-webkit-scrollbar{width:4px;height:8px;background-color:#f5f5f5}::-webkit-scrollbar-thumb,::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,.3);background-color:#f5f5f5}.el-menu .fa{vertical-align:middle;margin-right:5px;width:24px;text-align:center}.el-menu .fa:not(.is-children){font-size:14px}.gray-mode{-webkit-filter:grayscale(100%);filter:grayscale(100%)}.fade-enter-active,.fade-leave-active{transition:opacity .2s ease-in-out}.fade-enter,.fade-leave-to{opacity:0}.none-select{moz-user-select:-moz-none;-moz-user-select:none;-o-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.toolbar{width:100%;padding:8px;background-color:#fff;overflow:hidden;line-height:32px;border:1px solid #e6ebf5}.fl{float:left}

View File

@@ -0,0 +1 @@
.login{display:flex;justify-content:center;align-items:center;position:absolute;height:100%;width:100%;background-color:#e4e5e6}.login .login-form{width:375px;height:435px;padding:30px;background-color:#fff;text-align:left;border-radius:4px;position:relative;margin-left:0;margin-right:0;zoom:1;display:block}.login .login-form .login-header{text-align:center;font-size:16px;font-weight:700;margin-bottom:20px}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-6c422708"],{"9d64":function(e,t,n){e.exports=n.p+"static/img/logo.e92f231a.png"},a248:function(e,t,n){"use strict";var r=n("df3e"),a=n.n(r);a.a},df3e:function(e,t,n){},ede4:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"login"},[n("div",{staticClass:"login-form"},[e._m(0),n("el-input",{staticStyle:{"margin-bottom":"18px"},attrs:{placeholder:"请输入用户名","suffix-icon":"fa fa-user"},model:{value:e.loginForm.username,callback:function(t){e.$set(e.loginForm,"username",t)},expression:"loginForm.username"}}),n("el-input",{staticStyle:{"margin-bottom":"18px"},attrs:{placeholder:"请输入密码","suffix-icon":"fa fa-keyboard-o",type:"password",autocomplete:"new-password"},model:{value:e.loginForm.password,callback:function(t){e.$set(e.loginForm,"password",t)},expression:"loginForm.password"}}),n("el-button",{staticStyle:{width:"100%","margin-bottom":"18px"},attrs:{type:"primary",loading:e.loginLoading},nativeOn:{click:function(t){return e.login(t)}}},[e._v("登录")]),n("div",[n("el-checkbox",{model:{value:e.remember,callback:function(t){e.remember=t},expression:"remember"}},[e._v("记住密码")])],1)],1)])},a=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"login-header"},[r("img",{attrs:{src:n("9d64"),width:"150",height:"120",alt:""}})])}],o=(n("6a61"),n("cf7f")),i=n("1462"),s=n("a340"),u=n("bb06"),c=n("9691"),l=n("0372"),m=n("d789"),g={login:function(e){return m["a"].request("POST","/accounts/login",e,null)},captcha:function(){return m["a"].request("GET","/open/captcha",null,null)},logout:function(e){return m["a"].request("POST","/sys/accounts/logout/{token}",e,null)}},p=n("e4a1"),f=n("79cb"),d=function(e){Object(u["a"])(n,e);var t=Object(c["a"])(n);function n(){var e;return Object(i["a"])(this,n),e=t.apply(this,arguments),e.loginForm={username:"",password:"",uuid:""},e.remember=!1,e.loginLoading=!1,e}return Object(s["a"])(n,[{key:"mounted",value:function(){var e,t=this.getRemember();null!=t&&(e=JSON.parse(t)),e?(this.remember=!0,this.loginForm.username=e.username,this.loginForm.password=e.password):this.remember=!1}},{key:"getCaptcha",value:function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(){var t;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,g.captcha();case 2:t=e.sent,this.loginForm.uuid=t.uuid;case 4:case"end":return e.stop()}}),e,this)})));function t(){return e.apply(this,arguments)}return t}()},{key:"login",value:function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(){var t,n=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return this.loginLoading=!0,e.prev=1,e.next=4,g.login(this.loginForm);case 4:t=e.sent,this.remember?localStorage.setItem("remember",JSON.stringify(this.loginForm)):localStorage.removeItem("remember"),setTimeout((function(){f["a"].saveToken(t.token),n.$notify({title:"登录成功",message:"很高兴你使用Mayfly Admin别忘了给个Star哦。",type:"success"}),n.loginLoading=!1;var e=n.$route.query.redirect;e?n.$router.push(e):n.$router.push({path:"/"})}),500),e.next=12;break;case 9:e.prev=9,e.t0=e["catch"](1),this.loginLoading=!1;case 12:case"end":return e.stop()}}),e,this,[[1,9]])})));function t(){return e.apply(this,arguments)}return t}()},{key:"getRemember",value:function(){return localStorage.getItem("remember")}}]),n}(p["c"]);d=Object(l["a"])([Object(p["a"])({name:"Login"})],d);var h=d,b=h,v=(n("a248"),n("9ca4")),w=Object(v["a"])(b,r,a,!1,null,null,null);t["default"]=w.exports}}]);

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