mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
first
This commit is contained in:
41
base/assert.go
Normal file
41
base/assert.go
Normal 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
27
base/bizerror.go
Normal 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
141
base/controller.go
Normal 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)
|
||||
}
|
||||
}
|
||||
161
base/httpclient/httpclient.go
Normal file
161
base/httpclient/httpclient.go
Normal 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
216
base/model.go
Normal 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
71
base/model_test.go
Normal 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
13
base/page.go
Normal 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
66
base/result.go
Normal 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
49
base/token.go
Normal 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
23
base/utils/map_utils.go
Normal 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
89
base/utils/str_utils.go
Normal 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
629
base/utils/struct_utils.go
Normal 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())
|
||||
}
|
||||
195
base/utils/struct_utils_test.go
Normal file
195
base/utils/struct_utils_test.go
Normal 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
21
conf/app.conf
Normal 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
42
controllers/account.go
Normal 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
12
controllers/form/form.go
Normal 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
90
controllers/machine.go
Normal 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
31
controllers/vo/vo.go
Normal 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
21
go.mod
Normal 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
95
go.sum
Normal 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
188
machine/machine.go
Normal 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
142
machine/machine_test.go
Normal 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
54
machine/shell.go
Normal 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
|
||||
}
|
||||
23
machine/shell/get_process_by_name.sh
Normal file
23
machine/shell/get_process_by_name.sh
Normal 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
13
machine/shell/monitor.sh
Normal 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
192
machine/shell/sys_info.sh
Normal 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
|
||||
41
machine/shell/system_info.sh
Normal file
41
machine/shell/system_info.sh
Normal 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
105
machine/status.go
Normal 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"`
|
||||
// 空闲的(idle)CPU时间占比
|
||||
CpuId float32 `json:"cpuId"`
|
||||
// 等待(wait)I/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
261
machine/ws_shell_session.go
Normal 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
42
main.go
Normal 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)
|
||||
}
|
||||
3
mayfly-go-front/.browserslistrc
Normal file
3
mayfly-go-front/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
5
mayfly-go-front/.env.development
Normal file
5
mayfly-go-front/.env.development
Normal file
@@ -0,0 +1,5 @@
|
||||
# just a flag
|
||||
ENV = 'development'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = 'http://localhost:8888/api'
|
||||
5
mayfly-go-front/.env.production
Normal file
5
mayfly-go-front/.env.production
Normal file
@@ -0,0 +1,5 @@
|
||||
# just a flag
|
||||
ENV = 'production'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = 'http://localhost:8888/api'
|
||||
18
mayfly-go-front/.eslintrc.js
Normal file
18
mayfly-go-front/.eslintrc.js
Normal 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
23
mayfly-go-front/.gitignore
vendored
Normal 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
24
mayfly-go-front/README.md
Normal 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/).
|
||||
5
mayfly-go-front/babel.config.js
Normal file
5
mayfly-go-front/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
12584
mayfly-go-front/package-lock.json
generated
Normal file
12584
mayfly-go-front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
mayfly-go-front/package.json
Normal file
41
mayfly-go-front/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
mayfly-go-front/public/favicon.ico
Normal file
BIN
mayfly-go-front/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
mayfly-go-front/public/index.html
Normal file
17
mayfly-go-front/public/index.html
Normal 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>
|
||||
11
mayfly-go-front/src/App.vue
Normal file
11
mayfly-go-front/src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
background-color: #222d32;
|
||||
}
|
||||
</style>
|
||||
75
mayfly-go-front/src/assets/css/style.css
Executable file
75
mayfly-go-front/src/assets/css/style.css
Executable 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;
|
||||
}
|
||||
BIN
mayfly-go-front/src/assets/images/favicon.ico
Normal file
BIN
mayfly-go-front/src/assets/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
mayfly-go-front/src/assets/images/logo.png
Normal file
BIN
mayfly-go-front/src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
77
mayfly-go-front/src/common/Api.ts
Normal file
77
mayfly-go-front/src/common/Api.ts
Normal 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
|
||||
26
mayfly-go-front/src/common/AuthUtils.ts
Normal file
26
mayfly-go-front/src/common/AuthUtils.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
5
mayfly-go-front/src/common/config.ts
Normal file
5
mayfly-go-front/src/common/config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
baseApiUrl: process.env.VUE_APP_BASE_API
|
||||
}
|
||||
|
||||
export default config
|
||||
27
mayfly-go-front/src/common/enums.ts
Normal file
27
mayfly-go-front/src/common/enums.ts
Normal 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)
|
||||
// }
|
||||
105
mayfly-go-front/src/common/filter/index.ts
Normal file
105
mayfly-go-front/src/common/filter/index.ts
Normal 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 转换规则,可对传入object,string,function
|
||||
// * 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;
|
||||
// }
|
||||
// }
|
||||
7
mayfly-go-front/src/common/openApi.ts
Normal file
7
mayfly-go-front/src/common/openApi.ts
Normal 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)
|
||||
}
|
||||
183
mayfly-go-front/src/common/request.ts
Executable file
183
mayfly-go-front/src/common/request.ts
Executable 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
|
||||
}
|
||||
96
mayfly-go-front/src/common/util.ts
Normal file
96
mayfly-go-front/src/common/util.ts
Normal 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())}`;
|
||||
}
|
||||
64
mayfly-go-front/src/components/chart/ActivePlate.vue
Normal file
64
mayfly-go-front/src/components/chart/ActivePlate.vue
Normal 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>
|
||||
39
mayfly-go-front/src/components/chart/BaseChart.vue
Normal file
39
mayfly-go-front/src/components/chart/BaseChart.vue
Normal 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>
|
||||
43
mayfly-go-front/src/components/chart/Card.vue
Normal file
43
mayfly-go-front/src/components/chart/Card.vue
Normal 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>
|
||||
138
mayfly-go-front/src/components/chart/ChartBar.vue
Normal file
138
mayfly-go-front/src/components/chart/ChartBar.vue
Normal 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>
|
||||
92
mayfly-go-front/src/components/chart/ChartContinuou.vue
Normal file
92
mayfly-go-front/src/components/chart/ChartContinuou.vue
Normal 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>
|
||||
104
mayfly-go-front/src/components/chart/ChartFunnel.vue
Normal file
104
mayfly-go-front/src/components/chart/ChartFunnel.vue
Normal 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>
|
||||
89
mayfly-go-front/src/components/chart/ChartGauge.vue
Normal file
89
mayfly-go-front/src/components/chart/ChartGauge.vue
Normal 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>
|
||||
121
mayfly-go-front/src/components/chart/ChartLine.vue
Normal file
121
mayfly-go-front/src/components/chart/ChartLine.vue
Normal 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>
|
||||
111
mayfly-go-front/src/components/chart/ChartPie.vue
Normal file
111
mayfly-go-front/src/components/chart/ChartPie.vue
Normal 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>
|
||||
37
mayfly-go-front/src/components/chart/onoff.js
Normal file
37
mayfly-go-front/src/components/chart/onoff.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
490
mayfly-go-front/src/components/chart/theme.json
Normal file
490
mayfly-go-front/src/components/chart/theme.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
142
mayfly-go-front/src/components/dynamic-form/DynamicForm.vue
Executable file
142
mayfly-go-front/src/components/dynamic-form/DynamicForm.vue
Executable 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>
|
||||
@@ -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>
|
||||
2
mayfly-go-front/src/components/dynamic-form/index.js
Normal file
2
mayfly-go-front/src/components/dynamic-form/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as DynamicForm } from './DynamicForm.vue';
|
||||
export { default as DynamicFormDialog } from './DynamicFormDialog.vue';
|
||||
337
mayfly-go-front/src/layout/Layout.vue
Normal file
337
mayfly-go-front/src/layout/Layout.vue
Normal 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>
|
||||
42
mayfly-go-front/src/layout/MenuTree.vue
Normal file
42
mayfly-go-front/src/layout/MenuTree.vue
Normal 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>>
|
||||
25
mayfly-go-front/src/main.ts
Normal file
25
mayfly-go-front/src/main.ts
Normal 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')
|
||||
57
mayfly-go-front/src/router/index.ts
Normal file
57
mayfly-go-front/src/router/index.ts
Normal 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
13
mayfly-go-front/src/shims-tsx.d.ts
vendored
Normal 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
4
mayfly-go-front/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
||||
15
mayfly-go-front/src/store/index.ts
Normal file
15
mayfly-go-front/src/store/index.ts
Normal 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: {
|
||||
}
|
||||
})
|
||||
28
mayfly-go-front/src/views/login/Login.less
Executable file
28
mayfly-go-front/src/views/login/Login.less
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
145
mayfly-go-front/src/views/login/Login.vue
Executable file
145
mayfly-go-front/src/views/login/Login.vue
Executable 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>
|
||||
333
mayfly-go-front/src/views/machine/MachineList.vue
Normal file
333
mayfly-go-front/src/views/machine/MachineList.vue
Normal 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>
|
||||
325
mayfly-go-front/src/views/machine/Monitor.vue
Executable file
325
mayfly-go-front/src/views/machine/Monitor.vue
Executable 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>
|
||||
25
mayfly-go-front/src/views/machine/api.ts
Normal file
25
mayfly-go-front/src/views/machine/api.ts
Normal 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'),
|
||||
}
|
||||
1
mayfly-go-front/src/views/machine/index.ts
Normal file
1
mayfly-go-front/src/views/machine/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './MachineList.vue';
|
||||
45
mayfly-go-front/tsconfig.json
Normal file
45
mayfly-go-front/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
80
mayfly-go-front/vue.config.js
Normal file
80
mayfly-go-front/vue.config.js
Normal 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
25
models/account.go
Normal 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
45
models/machine.go
Normal 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
19
models/machine_monitor.go
Normal 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
19
models/role.go
Normal 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
23
routers/router.go
Normal 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
30
scheudler/mytask.go
Normal 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
28
scheudler/scheduler.go
Normal 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
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1
static/index.html
Normal file
1
static/index.html
Normal 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>
|
||||
1
static/static/css/app.e8323368.css
Normal file
1
static/static/css/app.e8323368.css
Normal 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}
|
||||
1
static/static/css/chunk-6c422708.2d81c5bb.css
Normal file
1
static/static/css/chunk-6c422708.2d81c5bb.css
Normal 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}
|
||||
1
static/static/css/chunk-vendors.08810481.css
Normal file
1
static/static/css/chunk-vendors.08810481.css
Normal file
File diff suppressed because one or more lines are too long
BIN
static/static/fonts/element-icons.535877f5.woff
Normal file
BIN
static/static/fonts/element-icons.535877f5.woff
Normal file
Binary file not shown.
BIN
static/static/fonts/element-icons.732389de.ttf
Normal file
BIN
static/static/fonts/element-icons.732389de.ttf
Normal file
Binary file not shown.
BIN
static/static/img/logo.e92f231a.png
Normal file
BIN
static/static/img/logo.e92f231a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
1
static/static/js/app.aa8651f8.js
Normal file
1
static/static/js/app.aa8651f8.js
Normal file
File diff suppressed because one or more lines are too long
1
static/static/js/chunk-4a3c1aef.94cc2a02.js
Normal file
1
static/static/js/chunk-4a3c1aef.94cc2a02.js
Normal file
File diff suppressed because one or more lines are too long
1
static/static/js/chunk-6c422708.a09466dd.js
Normal file
1
static/static/js/chunk-6c422708.a09466dd.js
Normal 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
Reference in New Issue
Block a user