├── .gitignore ├── README.md ├── app ├── controllers │ ├── base.go │ ├── group.go │ ├── help.go │ ├── main.go │ └── task.go ├── jobs │ ├── cron.go │ ├── init.go │ └── job.go ├── libs │ ├── pager.go │ └── string.go ├── mail │ └── mail.go └── models │ ├── init.go │ ├── task.go │ ├── task_group.go │ ├── task_log.go │ └── user.go ├── conf └── app.conf ├── install.sql ├── main.go ├── pack.sh ├── run.sh ├── screenshot.png ├── static ├── css │ ├── base-admin-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── font-awesome-ie7.css │ ├── font-awesome-ie7.min.css │ ├── font-awesome.css │ ├── font-awesome.min.css │ ├── pages │ │ ├── dashboard.css │ │ ├── faq.css │ │ ├── plans.css │ │ ├── reports.css │ │ └── signin.css │ └── style.css ├── font │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.svgz │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfontd41d.eot ├── img │ ├── body-bg.png │ ├── glyphicons-halflings-white.html │ ├── glyphicons-halflings.html │ ├── icons-sa7c41345d9.png │ └── signin │ │ ├── check.png │ │ ├── fb_btn.png │ │ ├── password.png │ │ ├── twitter_btn.png │ │ └── user.png └── js │ ├── base.js │ ├── bootstrap.js │ ├── chart.min.js │ ├── charts │ ├── area.js │ ├── bar.js │ ├── donut.js │ ├── line.js │ └── pie.js │ ├── excanvas.min.js │ ├── full-calendar │ ├── fullcalendar.css │ └── fullcalendar.min.js │ ├── guidely │ ├── guidely-number.png │ ├── guidely.css │ └── guidely.min.js │ └── jquery-1.7.2.min.js └── views ├── error ├── 404.html └── message.html ├── group ├── add.html ├── edit.html └── list.html ├── help └── index.html ├── layout └── layout.html ├── main ├── index.html ├── login.html └── profile.html └── task ├── add.html ├── edit.html ├── list.html ├── logs.html └── viewlog.html /.gitignore: -------------------------------------------------------------------------------- 1 | webcron* 2 | bin/ 3 | *.tar.gz 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webcron 2 | ------------ 3 | 4 | 一个定时任务管理器,基于Go语言和beego框架开发。用于统一管理项目中的定时任务,提供可视化配置界面、执行日志记录、邮件通知等功能,无需依赖*unix下的crontab服务。 5 | 6 | ## 项目背景 7 | 8 | 开发此项目是为了解决本人所在公司的PHP项目中定时任务繁多,使用crontab不好管理的问题。我所在项目的定时任务也是PHP编写的,属于整个项目的一部分,我希望能有一个系统可以统一配置这些定时任务,并且可以查看每次任务的执行情况,任务执行完成或失败能够自动邮件提醒开发人员,因此做了这个项目。 9 | 10 | ## 功能特点 11 | 12 | * 统一管理多种定时任务。 13 | * 秒级定时器,使用crontab的时间表达式。 14 | * 可随时暂停任务。 15 | * 记录每次任务的执行结果。 16 | * 执行结果邮件通知。 17 | 18 | ## 界面截图 19 | 20 | ![webcron](https://raw.githubusercontent.com/lisijie/webcron/master/screenshot.png) 21 | 22 | 23 | ## 安装说明 24 | 25 | 系统需要安装Go和MySQL。 26 | 27 | 获取源码 28 | 29 | $ go get github.com/lisijie/webcron 30 | 31 | 打开配置文件 conf/app.conf,修改相关配置。 32 | 33 | 34 | 创建数据库webcron,再导入install.sql 35 | 36 | $ mysql -u username -p -D webcron < install.sql 37 | 38 | 运行 39 | 40 | $ ./webcron 41 | 或 42 | $ nohup ./webcron 2>&1 > error.log & 43 | 设为后台运行 44 | 45 | 访问: 46 | 47 | http://localhost:8000 48 | 49 | 帐号:admin 50 | 密码:admin888 -------------------------------------------------------------------------------- /app/controllers/base.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/webcron/app/libs" 6 | "github.com/lisijie/webcron/app/models" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | MSG_OK = 0 13 | MSG_ERR = -1 14 | ) 15 | 16 | type BaseController struct { 17 | beego.Controller 18 | controllerName string 19 | actionName string 20 | user *models.User 21 | userId int 22 | userName string 23 | pageSize int 24 | } 25 | 26 | func (this *BaseController) Prepare() { 27 | this.pageSize = 20 28 | controllerName, actionName := this.GetControllerAndAction() 29 | this.controllerName = strings.ToLower(controllerName[0 : len(controllerName)-10]) 30 | this.actionName = strings.ToLower(actionName) 31 | this.auth() 32 | 33 | this.Data["version"] = beego.AppConfig.String("version") 34 | this.Data["siteName"] = beego.AppConfig.String("site.name") 35 | this.Data["curRoute"] = this.controllerName + "." + this.actionName 36 | this.Data["curController"] = this.controllerName 37 | this.Data["curAction"] = this.actionName 38 | this.Data["loginUserId"] = this.userId 39 | this.Data["loginUserName"] = this.userName 40 | } 41 | 42 | //登录状态验证 43 | func (this *BaseController) auth() { 44 | arr := strings.Split(this.Ctx.GetCookie("auth"), "|") 45 | if len(arr) == 2 { 46 | idstr, password := arr[0], arr[1] 47 | userId, _ := strconv.Atoi(idstr) 48 | if userId > 0 { 49 | user, err := models.UserGetById(userId) 50 | if err == nil && password == libs.Md5([]byte(this.getClientIp()+"|"+user.Password+user.Salt)) { 51 | this.userId = user.Id 52 | this.userName = user.UserName 53 | this.user = user 54 | } 55 | } 56 | } 57 | 58 | if this.userId == 0 && (this.controllerName != "main" || 59 | (this.controllerName == "main" && this.actionName != "logout" && this.actionName != "login")) { 60 | this.redirect(beego.URLFor("MainController.Login")) 61 | } 62 | } 63 | 64 | //渲染模版 65 | func (this *BaseController) display(tpl ...string) { 66 | var tplname string 67 | if len(tpl) > 0 { 68 | tplname = tpl[0] + ".html" 69 | } else { 70 | tplname = this.controllerName + "/" + this.actionName + ".html" 71 | } 72 | this.Layout = "layout/layout.html" 73 | this.TplName = tplname 74 | } 75 | 76 | // 重定向 77 | func (this *BaseController) redirect(url string) { 78 | this.Redirect(url, 302) 79 | this.StopRun() 80 | } 81 | 82 | // 是否POST提交 83 | func (this *BaseController) isPost() bool { 84 | return this.Ctx.Request.Method == "POST" 85 | } 86 | 87 | // 显示错误信息 88 | func (this *BaseController) showMsg(args ...string) { 89 | this.Data["message"] = args[0] 90 | redirect := this.Ctx.Request.Referer() 91 | if len(args) > 1 { 92 | redirect = args[1] 93 | } 94 | 95 | this.Data["redirect"] = redirect 96 | this.Data["pageTitle"] = "系统提示" 97 | this.display("error/message") 98 | this.Render() 99 | this.StopRun() 100 | } 101 | 102 | // 输出json 103 | func (this *BaseController) jsonResult(out interface{}) { 104 | this.Data["json"] = out 105 | this.ServeJSON() 106 | this.StopRun() 107 | } 108 | 109 | func (this *BaseController) ajaxMsg(msg interface{}, msgno int) { 110 | out := make(map[string]interface{}) 111 | out["status"] = msgno 112 | out["msg"] = msg 113 | 114 | this.jsonResult(out) 115 | } 116 | 117 | //获取用户IP地址 118 | func (this *BaseController) getClientIp() string { 119 | s := strings.Split(this.Ctx.Request.RemoteAddr, ":") 120 | return s[0] 121 | } 122 | -------------------------------------------------------------------------------- /app/controllers/group.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/webcron/app/libs" 6 | "github.com/lisijie/webcron/app/models" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type GroupController struct { 12 | BaseController 13 | } 14 | 15 | func (this *GroupController) List() { 16 | page, _ := this.GetInt("page") 17 | if page < 1 { 18 | page = 1 19 | } 20 | 21 | list, count := models.TaskGroupGetList(page, this.pageSize) 22 | 23 | this.Data["pageTitle"] = "分组列表" 24 | this.Data["list"] = list 25 | this.Data["pageBar"] = libs.NewPager(page, int(count), this.pageSize, beego.URLFor("GroupController.List"), true).ToString() 26 | this.display() 27 | } 28 | 29 | func (this *GroupController) Add() { 30 | if this.isPost() { 31 | group := new(models.TaskGroup) 32 | group.GroupName = strings.TrimSpace(this.GetString("group_name")) 33 | group.UserId = this.userId 34 | group.Description = strings.TrimSpace(this.GetString("description")) 35 | 36 | _, err := models.TaskGroupAdd(group) 37 | if err != nil { 38 | this.ajaxMsg(err.Error(), MSG_ERR) 39 | } 40 | this.ajaxMsg("", MSG_OK) 41 | } 42 | 43 | this.Data["pageTitle"] = "添加分组" 44 | this.display() 45 | } 46 | 47 | func (this *GroupController) Edit() { 48 | id, _ := this.GetInt("id") 49 | 50 | group, err := models.TaskGroupGetById(id) 51 | if err != nil { 52 | this.showMsg(err.Error()) 53 | } 54 | 55 | if this.isPost() { 56 | group.GroupName = strings.TrimSpace(this.GetString("group_name")) 57 | group.Description = strings.TrimSpace(this.GetString("description")) 58 | err := group.Update() 59 | if err != nil { 60 | this.ajaxMsg(err.Error(), MSG_ERR) 61 | } 62 | this.ajaxMsg("", MSG_OK) 63 | } 64 | 65 | this.Data["pageTitle"] = "编辑分组" 66 | this.Data["group"] = group 67 | this.display() 68 | } 69 | 70 | func (this *GroupController) Batch() { 71 | action := this.GetString("action") 72 | ids := this.GetStrings("ids") 73 | if len(ids) < 1 { 74 | this.ajaxMsg("请选择要操作的项目", MSG_ERR) 75 | } 76 | 77 | for _, v := range ids { 78 | id, _ := strconv.Atoi(v) 79 | if id < 1 { 80 | continue 81 | } 82 | switch action { 83 | case "delete": 84 | models.TaskGroupDelById(id) 85 | models.TaskResetGroupId(id) 86 | } 87 | } 88 | 89 | this.ajaxMsg("", MSG_OK) 90 | } 91 | -------------------------------------------------------------------------------- /app/controllers/help.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | type HelpController struct { 4 | BaseController 5 | } 6 | 7 | func (this *HelpController) Index() { 8 | 9 | this.Data["pageTitle"] = "使用帮助" 10 | this.display() 11 | } 12 | -------------------------------------------------------------------------------- /app/controllers/main.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/astaxie/beego/utils" 6 | "github.com/lisijie/webcron/app/jobs" 7 | "github.com/lisijie/webcron/app/libs" 8 | "github.com/lisijie/webcron/app/models" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type MainController struct { 16 | BaseController 17 | } 18 | 19 | // 首页 20 | func (this *MainController) Index() { 21 | this.Data["pageTitle"] = "系统概况" 22 | 23 | // 即将执行的任务 24 | entries := jobs.GetEntries(30) 25 | jobList := make([]map[string]interface{}, len(entries)) 26 | for k, v := range entries { 27 | row := make(map[string]interface{}) 28 | job := v.Job.(*jobs.Job) 29 | row["task_id"] = job.GetId() 30 | row["task_name"] = job.GetName() 31 | row["next_time"] = beego.Date(v.Next, "Y-m-d H:i:s") 32 | jobList[k] = row 33 | } 34 | 35 | // 最近执行的日志 36 | logs, _ := models.TaskLogGetList(1, 20) 37 | recentLogs := make([]map[string]interface{}, len(logs)) 38 | for k, v := range logs { 39 | task, err := models.TaskGetById(v.TaskId) 40 | taskName := "" 41 | if err == nil { 42 | taskName = task.TaskName 43 | } 44 | row := make(map[string]interface{}) 45 | row["task_name"] = taskName 46 | row["id"] = v.Id 47 | row["start_time"] = beego.Date(time.Unix(v.CreateTime, 0), "Y-m-d H:i:s") 48 | row["process_time"] = float64(v.ProcessTime) / 1000 49 | row["ouput_size"] = libs.SizeFormat(float64(len(v.Output))) 50 | row["output"] = beego.Substr(v.Output, 0, 100) 51 | row["status"] = v.Status 52 | recentLogs[k] = row 53 | } 54 | 55 | // 最近执行失败的日志 56 | logs, _ = models.TaskLogGetList(1, 20, "status__lt", 0) 57 | errLogs := make([]map[string]interface{}, len(logs)) 58 | for k, v := range logs { 59 | task, err := models.TaskGetById(v.TaskId) 60 | taskName := "" 61 | if err == nil { 62 | taskName = task.TaskName 63 | } 64 | row := make(map[string]interface{}) 65 | row["task_name"] = taskName 66 | row["id"] = v.Id 67 | row["start_time"] = beego.Date(time.Unix(v.CreateTime, 0), "Y-m-d H:i:s") 68 | row["process_time"] = float64(v.ProcessTime) / 1000 69 | row["ouput_size"] = libs.SizeFormat(float64(len(v.Output))) 70 | row["error"] = beego.Substr(v.Error, 0, 100) 71 | row["status"] = v.Status 72 | errLogs[k] = row 73 | } 74 | 75 | this.Data["recentLogs"] = recentLogs 76 | this.Data["errLogs"] = errLogs 77 | this.Data["jobs"] = jobList 78 | this.Data["cpuNum"] = runtime.NumCPU() 79 | this.display() 80 | } 81 | 82 | // 个人信息 83 | func (this *MainController) Profile() { 84 | beego.ReadFromRequest(&this.Controller) 85 | user, _ := models.UserGetById(this.userId) 86 | 87 | if this.isPost() { 88 | flash := beego.NewFlash() 89 | user.Email = this.GetString("email") 90 | user.Update() 91 | password1 := this.GetString("password1") 92 | password2 := this.GetString("password2") 93 | if password1 != "" { 94 | if len(password1) < 6 { 95 | flash.Error("密码长度必须大于6位") 96 | flash.Store(&this.Controller) 97 | this.redirect(beego.URLFor(".Profile")) 98 | } else if password2 != password1 { 99 | flash.Error("两次输入的密码不一致") 100 | flash.Store(&this.Controller) 101 | this.redirect(beego.URLFor(".Profile")) 102 | } else { 103 | user.Salt = string(utils.RandomCreateBytes(10)) 104 | user.Password = libs.Md5([]byte(password1 + user.Salt)) 105 | user.Update() 106 | } 107 | } 108 | flash.Success("修改成功!") 109 | flash.Store(&this.Controller) 110 | this.redirect(beego.URLFor(".Profile")) 111 | } 112 | 113 | this.Data["pageTitle"] = "个人信息" 114 | this.Data["user"] = user 115 | this.display() 116 | } 117 | 118 | // 登录 119 | func (this *MainController) Login() { 120 | if this.userId > 0 { 121 | this.redirect("/") 122 | } 123 | beego.ReadFromRequest(&this.Controller) 124 | if this.isPost() { 125 | flash := beego.NewFlash() 126 | 127 | username := strings.TrimSpace(this.GetString("username")) 128 | password := strings.TrimSpace(this.GetString("password")) 129 | remember := this.GetString("remember") 130 | if username != "" && password != "" { 131 | user, err := models.UserGetByName(username) 132 | errorMsg := "" 133 | if err != nil || user.Password != libs.Md5([]byte(password+user.Salt)) { 134 | errorMsg = "帐号或密码错误" 135 | } else if user.Status == -1 { 136 | errorMsg = "该帐号已禁用" 137 | } else { 138 | user.LastIp = this.getClientIp() 139 | user.LastLogin = time.Now().Unix() 140 | models.UserUpdate(user) 141 | 142 | authkey := libs.Md5([]byte(this.getClientIp() + "|" + user.Password + user.Salt)) 143 | if remember == "yes" { 144 | this.Ctx.SetCookie("auth", strconv.Itoa(user.Id)+"|"+authkey, 7*86400) 145 | } else { 146 | this.Ctx.SetCookie("auth", strconv.Itoa(user.Id)+"|"+authkey) 147 | } 148 | 149 | this.redirect(beego.URLFor("TaskController.List")) 150 | } 151 | flash.Error(errorMsg) 152 | flash.Store(&this.Controller) 153 | this.redirect(beego.URLFor("MainController.Login")) 154 | } 155 | } 156 | 157 | this.TplName = "main/login.html" 158 | } 159 | 160 | // 退出登录 161 | func (this *MainController) Logout() { 162 | this.Ctx.SetCookie("auth", "") 163 | this.redirect(beego.URLFor("MainController.Login")) 164 | } 165 | 166 | // 获取系统时间 167 | func (this *MainController) GetTime() { 168 | out := make(map[string]interface{}) 169 | out["time"] = time.Now().UnixNano() / int64(time.Millisecond) 170 | this.jsonResult(out) 171 | } 172 | -------------------------------------------------------------------------------- /app/controllers/task.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | libcron "github.com/lisijie/cron" 6 | "github.com/lisijie/webcron/app/jobs" 7 | "github.com/lisijie/webcron/app/libs" 8 | "github.com/lisijie/webcron/app/models" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type TaskController struct { 15 | BaseController 16 | } 17 | 18 | // 任务列表 19 | func (this *TaskController) List() { 20 | page, _ := this.GetInt("page") 21 | if page < 1 { 22 | page = 1 23 | } 24 | groupId, _ := this.GetInt("groupid") 25 | filters := make([]interface{}, 0) 26 | if groupId > 0 { 27 | filters = append(filters, "group_id", groupId) 28 | } 29 | result, count := models.TaskGetList(page, this.pageSize, filters...) 30 | 31 | list := make([]map[string]interface{}, len(result)) 32 | for k, v := range result { 33 | row := make(map[string]interface{}) 34 | row["id"] = v.Id 35 | row["name"] = v.TaskName 36 | row["cron_spec"] = v.CronSpec 37 | row["status"] = v.Status 38 | row["description"] = v.Description 39 | 40 | e := jobs.GetEntryById(v.Id) 41 | if e != nil { 42 | row["next_time"] = beego.Date(e.Next, "Y-m-d H:i:s") 43 | row["prev_time"] = "-" 44 | if e.Prev.Unix() > 0 { 45 | row["prev_time"] = beego.Date(e.Prev, "Y-m-d H:i:s") 46 | } else if v.PrevTime > 0 { 47 | row["prev_time"] = beego.Date(time.Unix(v.PrevTime, 0), "Y-m-d H:i:s") 48 | } 49 | row["running"] = 1 50 | } else { 51 | row["next_time"] = "-" 52 | if v.PrevTime > 0 { 53 | row["prev_time"] = beego.Date(time.Unix(v.PrevTime, 0), "Y-m-d H:i:s") 54 | } else { 55 | row["prev_time"] = "-" 56 | } 57 | row["running"] = 0 58 | } 59 | list[k] = row 60 | } 61 | 62 | // 分组列表 63 | groups, _ := models.TaskGroupGetList(1, 100) 64 | 65 | this.Data["pageTitle"] = "任务列表" 66 | this.Data["list"] = list 67 | this.Data["groups"] = groups 68 | this.Data["groupid"] = groupId 69 | this.Data["pageBar"] = libs.NewPager(page, int(count), this.pageSize, beego.URLFor("TaskController.List", "groupid", groupId), true).ToString() 70 | this.display() 71 | } 72 | 73 | // 添加任务 74 | func (this *TaskController) Add() { 75 | 76 | if this.isPost() { 77 | task := new(models.Task) 78 | task.UserId = this.userId 79 | task.GroupId, _ = this.GetInt("group_id") 80 | task.TaskName = strings.TrimSpace(this.GetString("task_name")) 81 | task.Description = strings.TrimSpace(this.GetString("description")) 82 | task.Concurrent, _ = this.GetInt("concurrent") 83 | task.CronSpec = strings.TrimSpace(this.GetString("cron_spec")) 84 | task.Command = strings.TrimSpace(this.GetString("command")) 85 | task.Notify, _ = this.GetInt("notify") 86 | task.Timeout, _ = this.GetInt("timeout") 87 | 88 | notifyEmail := strings.TrimSpace(this.GetString("notify_email")) 89 | if notifyEmail != "" { 90 | emailList := make([]string, 0) 91 | tmp := strings.Split(notifyEmail, "\n") 92 | for _, v := range tmp { 93 | v = strings.TrimSpace(v) 94 | if !libs.IsEmail([]byte(v)) { 95 | this.ajaxMsg("无效的Email地址:"+v, MSG_ERR) 96 | } else { 97 | emailList = append(emailList, v) 98 | } 99 | } 100 | task.NotifyEmail = strings.Join(emailList, "\n") 101 | } 102 | 103 | if task.TaskName == "" || task.CronSpec == "" || task.Command == "" { 104 | this.ajaxMsg("请填写完整信息", MSG_ERR) 105 | } 106 | if _, err := libcron.Parse(task.CronSpec); err != nil { 107 | this.ajaxMsg("cron表达式无效", MSG_ERR) 108 | } 109 | if _, err := models.TaskAdd(task); err != nil { 110 | this.ajaxMsg(err.Error(), MSG_ERR) 111 | } 112 | 113 | this.ajaxMsg("", MSG_OK) 114 | } 115 | 116 | // 分组列表 117 | groups, _ := models.TaskGroupGetList(1, 100) 118 | this.Data["groups"] = groups 119 | this.Data["pageTitle"] = "添加任务" 120 | this.display() 121 | } 122 | 123 | // 编辑任务 124 | func (this *TaskController) Edit() { 125 | id, _ := this.GetInt("id") 126 | 127 | task, err := models.TaskGetById(id) 128 | if err != nil { 129 | this.showMsg(err.Error()) 130 | } 131 | 132 | if this.isPost() { 133 | task.TaskName = strings.TrimSpace(this.GetString("task_name")) 134 | task.Description = strings.TrimSpace(this.GetString("description")) 135 | task.GroupId, _ = this.GetInt("group_id") 136 | task.Concurrent, _ = this.GetInt("concurrent") 137 | task.CronSpec = strings.TrimSpace(this.GetString("cron_spec")) 138 | task.Command = strings.TrimSpace(this.GetString("command")) 139 | task.Notify, _ = this.GetInt("notify") 140 | task.Timeout, _ = this.GetInt("timeout") 141 | 142 | notifyEmail := strings.TrimSpace(this.GetString("notify_email")) 143 | if notifyEmail != "" { 144 | tmp := strings.Split(notifyEmail, "\n") 145 | emailList := make([]string, 0, len(tmp)) 146 | for _, v := range tmp { 147 | v = strings.TrimSpace(v) 148 | if !libs.IsEmail([]byte(v)) { 149 | this.ajaxMsg("无效的Email地址:"+v, MSG_ERR) 150 | } else { 151 | emailList = append(emailList, v) 152 | } 153 | } 154 | task.NotifyEmail = strings.Join(emailList, "\n") 155 | } 156 | 157 | if task.TaskName == "" || task.CronSpec == "" || task.Command == "" { 158 | this.ajaxMsg("请填写完整信息", MSG_ERR) 159 | } 160 | if _, err := libcron.Parse(task.CronSpec); err != nil { 161 | this.ajaxMsg("cron表达式无效", MSG_ERR) 162 | } 163 | if err := task.Update(); err != nil { 164 | this.ajaxMsg(err.Error(), MSG_ERR) 165 | } 166 | 167 | this.ajaxMsg("", MSG_OK) 168 | } 169 | 170 | // 分组列表 171 | groups, _ := models.TaskGroupGetList(1, 100) 172 | this.Data["groups"] = groups 173 | this.Data["task"] = task 174 | this.Data["pageTitle"] = "编辑任务" 175 | this.display() 176 | } 177 | 178 | // 任务执行日志列表 179 | func (this *TaskController) Logs() { 180 | taskId, _ := this.GetInt("id") 181 | page, _ := this.GetInt("page") 182 | if page < 1 { 183 | page = 1 184 | } 185 | 186 | task, err := models.TaskGetById(taskId) 187 | if err != nil { 188 | this.showMsg(err.Error()) 189 | } 190 | 191 | result, count := models.TaskLogGetList(page, this.pageSize, "task_id", task.Id) 192 | 193 | list := make([]map[string]interface{}, len(result)) 194 | for k, v := range result { 195 | row := make(map[string]interface{}) 196 | row["id"] = v.Id 197 | row["start_time"] = beego.Date(time.Unix(v.CreateTime, 0), "Y-m-d H:i:s") 198 | row["process_time"] = float64(v.ProcessTime) / 1000 199 | row["ouput_size"] = libs.SizeFormat(float64(len(v.Output))) 200 | row["status"] = v.Status 201 | list[k] = row 202 | } 203 | 204 | this.Data["pageTitle"] = "任务执行日志" 205 | this.Data["list"] = list 206 | this.Data["task"] = task 207 | this.Data["pageBar"] = libs.NewPager(page, int(count), this.pageSize, beego.URLFor("TaskController.Logs", "id", taskId), true).ToString() 208 | this.display() 209 | } 210 | 211 | // 查看日志详情 212 | func (this *TaskController) ViewLog() { 213 | id, _ := this.GetInt("id") 214 | 215 | taskLog, err := models.TaskLogGetById(id) 216 | if err != nil { 217 | this.showMsg(err.Error()) 218 | } 219 | 220 | task, err := models.TaskGetById(taskLog.TaskId) 221 | if err != nil { 222 | this.showMsg(err.Error()) 223 | } 224 | 225 | data := make(map[string]interface{}) 226 | data["id"] = taskLog.Id 227 | data["output"] = taskLog.Output 228 | data["error"] = taskLog.Error 229 | data["start_time"] = beego.Date(time.Unix(taskLog.CreateTime, 0), "Y-m-d H:i:s") 230 | data["process_time"] = float64(taskLog.ProcessTime) / 1000 231 | data["ouput_size"] = libs.SizeFormat(float64(len(taskLog.Output))) 232 | data["status"] = taskLog.Status 233 | 234 | this.Data["task"] = task 235 | this.Data["data"] = data 236 | this.Data["pageTitle"] = "查看日志" 237 | this.display() 238 | } 239 | 240 | // 批量操作日志 241 | func (this *TaskController) LogBatch() { 242 | action := this.GetString("action") 243 | ids := this.GetStrings("ids") 244 | if len(ids) < 1 { 245 | this.ajaxMsg("请选择要操作的项目", MSG_ERR) 246 | } 247 | for _, v := range ids { 248 | id, _ := strconv.Atoi(v) 249 | if id < 1 { 250 | continue 251 | } 252 | switch action { 253 | case "delete": 254 | models.TaskLogDelById(id) 255 | } 256 | } 257 | 258 | this.ajaxMsg("", MSG_OK) 259 | } 260 | 261 | // 批量操作 262 | func (this *TaskController) Batch() { 263 | action := this.GetString("action") 264 | ids := this.GetStrings("ids") 265 | if len(ids) < 1 { 266 | this.ajaxMsg("请选择要操作的项目", MSG_ERR) 267 | } 268 | 269 | for _, v := range ids { 270 | id, _ := strconv.Atoi(v) 271 | if id < 1 { 272 | continue 273 | } 274 | switch action { 275 | case "active": 276 | if task, err := models.TaskGetById(id); err == nil { 277 | job, err := jobs.NewJobFromTask(task) 278 | if err == nil { 279 | jobs.AddJob(task.CronSpec, job) 280 | task.Status = 1 281 | task.Update() 282 | } 283 | } 284 | case "pause": 285 | jobs.RemoveJob(id) 286 | if task, err := models.TaskGetById(id); err == nil { 287 | task.Status = 0 288 | task.Update() 289 | } 290 | case "delete": 291 | models.TaskDel(id) 292 | models.TaskLogDelByTaskId(id) 293 | jobs.RemoveJob(id) 294 | } 295 | } 296 | 297 | this.ajaxMsg("", MSG_OK) 298 | } 299 | 300 | // 启动任务 301 | func (this *TaskController) Start() { 302 | id, _ := this.GetInt("id") 303 | 304 | task, err := models.TaskGetById(id) 305 | if err != nil { 306 | this.showMsg(err.Error()) 307 | } 308 | 309 | job, err := jobs.NewJobFromTask(task) 310 | if err != nil { 311 | this.showMsg(err.Error()) 312 | } 313 | 314 | if jobs.AddJob(task.CronSpec, job) { 315 | task.Status = 1 316 | task.Update() 317 | } 318 | 319 | refer := this.Ctx.Request.Referer() 320 | if refer == "" { 321 | refer = beego.URLFor("TaskController.List") 322 | } 323 | this.redirect(refer) 324 | } 325 | 326 | // 暂停任务 327 | func (this *TaskController) Pause() { 328 | id, _ := this.GetInt("id") 329 | 330 | task, err := models.TaskGetById(id) 331 | if err != nil { 332 | this.showMsg(err.Error()) 333 | } 334 | 335 | jobs.RemoveJob(id) 336 | task.Status = 0 337 | task.Update() 338 | 339 | refer := this.Ctx.Request.Referer() 340 | if refer == "" { 341 | refer = beego.URLFor("TaskController.List") 342 | } 343 | this.redirect(refer) 344 | } 345 | 346 | // 立即执行 347 | func (this *TaskController) Run() { 348 | id, _ := this.GetInt("id") 349 | 350 | task, err := models.TaskGetById(id) 351 | if err != nil { 352 | this.showMsg(err.Error()) 353 | } 354 | 355 | job, err := jobs.NewJobFromTask(task) 356 | if err != nil { 357 | this.showMsg(err.Error()) 358 | } 359 | 360 | job.Run() 361 | 362 | this.redirect(beego.URLFor("TaskController.ViewLog", "id", job.GetLogId())) 363 | } 364 | -------------------------------------------------------------------------------- /app/jobs/cron.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/cron" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | mainCron *cron.Cron 11 | workPool chan bool 12 | lock sync.Mutex 13 | ) 14 | 15 | func init() { 16 | if size, _ := beego.AppConfig.Int("jobs.pool"); size > 0 { 17 | workPool = make(chan bool, size) 18 | } 19 | mainCron = cron.New() 20 | mainCron.Start() 21 | } 22 | 23 | func AddJob(spec string, job *Job) bool { 24 | lock.Lock() 25 | defer lock.Unlock() 26 | 27 | if GetEntryById(job.id) != nil { 28 | return false 29 | } 30 | err := mainCron.AddJob(spec, job) 31 | if err != nil { 32 | beego.Error("AddJob: ", err.Error()) 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | func RemoveJob(id int) { 39 | mainCron.RemoveJob(func(e *cron.Entry) bool { 40 | if v, ok := e.Job.(*Job); ok { 41 | if v.id == id { 42 | return true 43 | } 44 | } 45 | return false 46 | }) 47 | } 48 | 49 | func GetEntryById(id int) *cron.Entry { 50 | entries := mainCron.Entries() 51 | for _, e := range entries { 52 | if v, ok := e.Job.(*Job); ok { 53 | if v.id == id { 54 | return e 55 | } 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func GetEntries(size int) []*cron.Entry { 62 | ret := mainCron.Entries() 63 | if len(ret) > size { 64 | return ret[:size] 65 | } 66 | return ret 67 | } 68 | -------------------------------------------------------------------------------- /app/jobs/init.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego" 6 | "github.com/lisijie/webcron/app/models" 7 | "os/exec" 8 | "time" 9 | ) 10 | 11 | func InitJobs() { 12 | list, _ := models.TaskGetList(1, 1000000, "status", 1) 13 | for _, task := range list { 14 | job, err := NewJobFromTask(task) 15 | if err != nil { 16 | beego.Error("InitJobs:", err.Error()) 17 | continue 18 | } 19 | AddJob(task.CronSpec, job) 20 | } 21 | } 22 | 23 | func runCmdWithTimeout(cmd *exec.Cmd, timeout time.Duration) (error, bool) { 24 | done := make(chan error) 25 | go func() { 26 | done <- cmd.Wait() 27 | }() 28 | 29 | var err error 30 | select { 31 | case <-time.After(timeout): 32 | beego.Warn(fmt.Sprintf("任务执行时间超过%d秒,进程将被强制杀掉: %d", int(timeout/time.Second), cmd.Process.Pid)) 33 | go func() { 34 | <-done // 读出上面的goroutine数据,避免阻塞导致无法退出 35 | }() 36 | if err = cmd.Process.Kill(); err != nil { 37 | beego.Error(fmt.Sprintf("进程无法杀掉: %d, 错误信息: %s", cmd.Process.Pid, err)) 38 | } 39 | return err, true 40 | case err = <-done: 41 | return err, false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/jobs/job.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/astaxie/beego" 7 | "github.com/lisijie/webcron/app/mail" 8 | "github.com/lisijie/webcron/app/models" 9 | "html/template" 10 | "os/exec" 11 | "runtime/debug" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var mailTpl *template.Template 17 | 18 | func init() { 19 | mailTpl, _ = template.New("mail_tpl").Parse(` 20 | 你好 {{.username}},
21 | 22 |

以下是任务执行结果:

23 | 24 |

25 | 任务ID:{{.task_id}}
26 | 任务名称:{{.task_name}}
27 | 执行时间:{{.start_time}}
28 | 执行耗时:{{.process_time}}秒
29 | 执行状态:{{.status}} 30 |

31 |

-------------以下是任务执行输出-------------

32 |

{{.output}}

33 |

34 | --------------------------------------------
35 | 本邮件由系统自动发出,请勿回复
36 | 如果要取消邮件通知,请登录到系统进行设置
37 |

38 | `) 39 | 40 | } 41 | 42 | type Job struct { 43 | id int // 任务ID 44 | logId int64 // 日志记录ID 45 | name string // 任务名称 46 | task *models.Task // 任务对象 47 | runFunc func(time.Duration) (string, string, error, bool) // 执行函数 48 | status int // 任务状态,大于0表示正在执行中 49 | Concurrent bool // 同一个任务是否允许并行执行 50 | } 51 | 52 | func NewJobFromTask(task *models.Task) (*Job, error) { 53 | if task.Id < 1 { 54 | return nil, fmt.Errorf("ToJob: 缺少id") 55 | } 56 | job := NewCommandJob(task.Id, task.TaskName, task.Command) 57 | job.task = task 58 | job.Concurrent = task.Concurrent == 1 59 | return job, nil 60 | } 61 | 62 | func NewCommandJob(id int, name string, command string) *Job { 63 | job := &Job{ 64 | id: id, 65 | name: name, 66 | } 67 | job.runFunc = func(timeout time.Duration) (string, string, error, bool) { 68 | bufOut := new(bytes.Buffer) 69 | bufErr := new(bytes.Buffer) 70 | cmd := exec.Command("/bin/bash", "-c", command) 71 | cmd.Stdout = bufOut 72 | cmd.Stderr = bufErr 73 | cmd.Start() 74 | err, isTimeout := runCmdWithTimeout(cmd, timeout) 75 | 76 | return bufOut.String(), bufErr.String(), err, isTimeout 77 | } 78 | return job 79 | } 80 | 81 | func (j *Job) Status() int { 82 | return j.status 83 | } 84 | 85 | func (j *Job) GetName() string { 86 | return j.name 87 | } 88 | 89 | func (j *Job) GetId() int { 90 | return j.id 91 | } 92 | 93 | func (j *Job) GetLogId() int64 { 94 | return j.logId 95 | } 96 | 97 | func (j *Job) Run() { 98 | if !j.Concurrent && j.status > 0 { 99 | beego.Warn(fmt.Sprintf("任务[%d]上一次执行尚未结束,本次被忽略。", j.id)) 100 | return 101 | } 102 | 103 | defer func() { 104 | if err := recover(); err != nil { 105 | beego.Error(err, "\n", string(debug.Stack())) 106 | } 107 | }() 108 | 109 | if workPool != nil { 110 | workPool <- true 111 | defer func() { 112 | <-workPool 113 | }() 114 | } 115 | 116 | beego.Debug(fmt.Sprintf("开始执行任务: %d", j.id)) 117 | 118 | j.status++ 119 | defer func() { 120 | j.status-- 121 | }() 122 | 123 | t := time.Now() 124 | timeout := time.Duration(time.Hour * 24) 125 | if j.task.Timeout > 0 { 126 | timeout = time.Second * time.Duration(j.task.Timeout) 127 | } 128 | 129 | cmdOut, cmdErr, err, isTimeout := j.runFunc(timeout) 130 | 131 | ut := time.Now().Sub(t) / time.Millisecond 132 | 133 | // 插入日志 134 | log := new(models.TaskLog) 135 | log.TaskId = j.id 136 | log.Output = cmdOut 137 | log.Error = cmdErr 138 | log.ProcessTime = int(ut) 139 | log.CreateTime = t.Unix() 140 | 141 | if isTimeout { 142 | log.Status = models.TASK_TIMEOUT 143 | log.Error = fmt.Sprintf("任务执行超过 %d 秒\n----------------------\n%s\n", int(timeout/time.Second), cmdErr) 144 | } else if err != nil { 145 | log.Status = models.TASK_ERROR 146 | log.Error = err.Error() + ":" + cmdErr 147 | } 148 | j.logId, _ = models.TaskLogAdd(log) 149 | 150 | // 更新上次执行时间 151 | j.task.PrevTime = t.Unix() 152 | j.task.ExecuteTimes++ 153 | j.task.Update("PrevTime", "ExecuteTimes") 154 | 155 | // 发送邮件通知 156 | if (j.task.Notify == 1 && err != nil) || j.task.Notify == 2 { 157 | user, uerr := models.UserGetById(j.task.UserId) 158 | if uerr != nil { 159 | return 160 | } 161 | 162 | var title string 163 | 164 | data := make(map[string]interface{}) 165 | data["task_id"] = j.task.Id 166 | data["username"] = user.UserName 167 | data["task_name"] = j.task.TaskName 168 | data["start_time"] = beego.Date(t, "Y-m-d H:i:s") 169 | data["process_time"] = float64(ut) / 1000 170 | data["output"] = cmdOut 171 | 172 | if isTimeout { 173 | title = fmt.Sprintf("任务执行结果通知 #%d: %s", j.task.Id, "超时") 174 | data["status"] = fmt.Sprintf("超时(%d秒)", int(timeout/time.Second)) 175 | } else if err != nil { 176 | title = fmt.Sprintf("任务执行结果通知 #%d: %s", j.task.Id, "失败") 177 | data["status"] = "失败(" + err.Error() + ")" 178 | } else { 179 | title = fmt.Sprintf("任务执行结果通知 #%d: %s", j.task.Id, "成功") 180 | data["status"] = "成功" 181 | } 182 | 183 | content := new(bytes.Buffer) 184 | mailTpl.Execute(content, data) 185 | ccList := make([]string, 0) 186 | if j.task.NotifyEmail != "" { 187 | ccList = strings.Split(j.task.NotifyEmail, "\n") 188 | } 189 | if !mail.SendMail(user.Email, user.UserName, title, content.String(), ccList) { 190 | beego.Error("发送邮件超时:", user.Email) 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/libs/pager.go: -------------------------------------------------------------------------------- 1 | package libs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strings" 8 | ) 9 | 10 | type Pager struct { 11 | Page int 12 | Totalnum int 13 | Pagesize int 14 | urlpath string 15 | urlquery string 16 | nopath bool 17 | } 18 | 19 | func NewPager(page, totalnum, pagesize int, url string, nopath ...bool) *Pager { 20 | p := new(Pager) 21 | p.Page = page 22 | p.Totalnum = totalnum 23 | p.Pagesize = pagesize 24 | 25 | arr := strings.Split(url, "?") 26 | p.urlpath = arr[0] 27 | if len(arr) > 1 { 28 | p.urlquery = "?" + arr[1] 29 | } else { 30 | p.urlquery = "" 31 | } 32 | 33 | if len(nopath) > 0 { 34 | p.nopath = nopath[0] 35 | } else { 36 | p.nopath = false 37 | } 38 | 39 | return p 40 | } 41 | 42 | func (this *Pager) url(page int) string { 43 | if this.nopath { //不使用目录形式 44 | if this.urlquery != "" { 45 | return fmt.Sprintf("%s%s&page=%d", this.urlpath, this.urlquery, page) 46 | } else { 47 | return fmt.Sprintf("%s?page=%d", this.urlpath, page) 48 | } 49 | } else { 50 | return fmt.Sprintf("%s/page/%d%s", this.urlpath, page, this.urlquery) 51 | } 52 | } 53 | 54 | func (this *Pager) ToString() string { 55 | if this.Totalnum <= this.Pagesize { 56 | return "" 57 | } 58 | 59 | var buf bytes.Buffer 60 | var from, to, linknum, offset, totalpage int 61 | 62 | offset = 5 63 | linknum = 10 64 | 65 | totalpage = int(math.Ceil(float64(this.Totalnum) / float64(this.Pagesize))) 66 | 67 | if totalpage < linknum { 68 | from = 1 69 | to = totalpage 70 | } else { 71 | from = this.Page - offset 72 | to = from + linknum 73 | if from < 1 { 74 | from = 1 75 | to = from + linknum - 1 76 | } else if to > totalpage { 77 | to = totalpage 78 | from = totalpage - linknum + 1 79 | } 80 | } 81 | 82 | buf.WriteString("") 111 | 112 | return buf.String() 113 | } 114 | -------------------------------------------------------------------------------- /app/libs/string.go: -------------------------------------------------------------------------------- 1 | package libs 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "regexp" 7 | ) 8 | 9 | var emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") 10 | 11 | func Md5(buf []byte) string { 12 | hash := md5.New() 13 | hash.Write(buf) 14 | return fmt.Sprintf("%x", hash.Sum(nil)) 15 | } 16 | 17 | func SizeFormat(size float64) string { 18 | units := []string{"Byte", "KB", "MB", "GB", "TB"} 19 | n := 0 20 | for size > 1024 { 21 | size /= 1024 22 | n += 1 23 | } 24 | 25 | return fmt.Sprintf("%.2f %s", size, units[n]) 26 | } 27 | 28 | func IsEmail(b []byte) bool { 29 | return emailPattern.Match(b) 30 | } 31 | -------------------------------------------------------------------------------- /app/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego" 6 | "github.com/astaxie/beego/utils" 7 | "time" 8 | ) 9 | 10 | var ( 11 | sendCh chan *utils.Email 12 | config string 13 | ) 14 | 15 | func init() { 16 | queueSize, _ := beego.AppConfig.Int("mail.queue_size") 17 | host := beego.AppConfig.String("mail.host") 18 | port, _ := beego.AppConfig.Int("mail.port") 19 | username := beego.AppConfig.String("mail.user") 20 | password := beego.AppConfig.String("mail.password") 21 | from := beego.AppConfig.String("mail.from") 22 | if port == 0 { 23 | port = 25 24 | } 25 | 26 | config = fmt.Sprintf(`{"username":"%s","password":"%s","host":"%s","port":%d,"from":"%s"}`, username, password, host, port, from) 27 | 28 | sendCh = make(chan *utils.Email, queueSize) 29 | 30 | go func() { 31 | for { 32 | select { 33 | case m, ok := <-sendCh: 34 | if !ok { 35 | return 36 | } 37 | if err := m.Send(); err != nil { 38 | beego.Error("SendMail:", err.Error()) 39 | } 40 | } 41 | } 42 | }() 43 | } 44 | 45 | func SendMail(address, name, subject, content string, cc []string) bool { 46 | mail := utils.NewEMail(config) 47 | mail.To = []string{address} 48 | mail.Subject = subject 49 | mail.HTML = content 50 | if len(cc) > 0 { 51 | mail.Cc = cc 52 | } 53 | 54 | select { 55 | case sendCh <- mail: 56 | return true 57 | case <-time.After(time.Second * 3): 58 | return false 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/models/init.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/astaxie/beego/orm" 6 | _ "github.com/go-sql-driver/mysql" 7 | "net/url" 8 | ) 9 | 10 | func Init() { 11 | dbhost := beego.AppConfig.String("db.host") 12 | dbport := beego.AppConfig.String("db.port") 13 | dbuser := beego.AppConfig.String("db.user") 14 | dbpassword := beego.AppConfig.String("db.password") 15 | dbname := beego.AppConfig.String("db.name") 16 | timezone := beego.AppConfig.String("db.timezone") 17 | if dbport == "" { 18 | dbport = "3306" 19 | } 20 | dsn := dbuser + ":" + dbpassword + "@tcp(" + dbhost + ":" + dbport + ")/" + dbname + "?charset=utf8" 21 | if timezone != "" { 22 | dsn = dsn + "&loc=" + url.QueryEscape(timezone) 23 | } 24 | orm.RegisterDataBase("default", "mysql", dsn) 25 | 26 | orm.RegisterModel(new(User), new(Task), new(TaskGroup), new(TaskLog)) 27 | 28 | if beego.AppConfig.String("runmode") == "dev" { 29 | orm.Debug = true 30 | } 31 | } 32 | 33 | func TableName(name string) string { 34 | return beego.AppConfig.String("db.prefix") + name 35 | } 36 | -------------------------------------------------------------------------------- /app/models/task.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "time" 7 | ) 8 | 9 | const ( 10 | TASK_SUCCESS = 0 // 任务执行成功 11 | TASK_ERROR = -1 // 任务执行出错 12 | TASK_TIMEOUT = -2 // 任务执行超时 13 | ) 14 | 15 | type Task struct { 16 | Id int 17 | UserId int 18 | GroupId int 19 | TaskName string 20 | TaskType int 21 | Description string 22 | CronSpec string 23 | Concurrent int 24 | Command string 25 | Status int 26 | Notify int 27 | NotifyEmail string 28 | Timeout int 29 | ExecuteTimes int 30 | PrevTime int64 31 | CreateTime int64 32 | } 33 | 34 | func (t *Task) TableName() string { 35 | return TableName("task") 36 | } 37 | 38 | func (t *Task) Update(fields ...string) error { 39 | if _, err := orm.NewOrm().Update(t, fields...); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func TaskAdd(task *Task) (int64, error) { 46 | if task.TaskName == "" { 47 | return 0, fmt.Errorf("TaskName字段不能为空") 48 | } 49 | if task.CronSpec == "" { 50 | return 0, fmt.Errorf("CronSpec字段不能为空") 51 | } 52 | if task.Command == "" { 53 | return 0, fmt.Errorf("Command字段不能为空") 54 | } 55 | if task.CreateTime == 0 { 56 | task.CreateTime = time.Now().Unix() 57 | } 58 | return orm.NewOrm().Insert(task) 59 | } 60 | 61 | func TaskGetList(page, pageSize int, filters ...interface{}) ([]*Task, int64) { 62 | offset := (page - 1) * pageSize 63 | 64 | tasks := make([]*Task, 0) 65 | 66 | query := orm.NewOrm().QueryTable(TableName("task")) 67 | if len(filters) > 0 { 68 | l := len(filters) 69 | for k := 0; k < l; k += 2 { 70 | query = query.Filter(filters[k].(string), filters[k+1]) 71 | } 72 | } 73 | total, _ := query.Count() 74 | query.OrderBy("-id").Limit(pageSize, offset).All(&tasks) 75 | 76 | return tasks, total 77 | } 78 | 79 | func TaskResetGroupId(groupId int) (int64, error) { 80 | return orm.NewOrm().QueryTable(TableName("task")).Filter("group_id", groupId).Update(orm.Params{ 81 | "group_id": 0, 82 | }) 83 | } 84 | 85 | func TaskGetById(id int) (*Task, error) { 86 | task := &Task{ 87 | Id: id, 88 | } 89 | 90 | err := orm.NewOrm().Read(task) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return task, nil 95 | } 96 | 97 | func TaskDel(id int) error { 98 | _, err := orm.NewOrm().QueryTable(TableName("task")).Filter("id", id).Delete() 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /app/models/task_group.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | ) 7 | 8 | type TaskGroup struct { 9 | Id int 10 | UserId int 11 | GroupName string 12 | Description string 13 | CreateTime int64 14 | } 15 | 16 | func (t *TaskGroup) TableName() string { 17 | return TableName("task_group") 18 | } 19 | 20 | func (t *TaskGroup) Update(fields ...string) error { 21 | if t.GroupName == "" { 22 | return fmt.Errorf("组名不能为空") 23 | } 24 | if _, err := orm.NewOrm().Update(t, fields...); err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func TaskGroupAdd(obj *TaskGroup) (int64, error) { 31 | if obj.GroupName == "" { 32 | return 0, fmt.Errorf("组名不能为空") 33 | } 34 | return orm.NewOrm().Insert(obj) 35 | } 36 | 37 | func TaskGroupGetById(id int) (*TaskGroup, error) { 38 | obj := &TaskGroup{ 39 | Id: id, 40 | } 41 | 42 | err := orm.NewOrm().Read(obj) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return obj, nil 47 | } 48 | 49 | func TaskGroupDelById(id int) error { 50 | _, err := orm.NewOrm().QueryTable(TableName("task_group")).Filter("id", id).Delete() 51 | return err 52 | } 53 | 54 | func TaskGroupGetList(page, pageSize int) ([]*TaskGroup, int64) { 55 | offset := (page - 1) * pageSize 56 | 57 | list := make([]*TaskGroup, 0) 58 | 59 | query := orm.NewOrm().QueryTable(TableName("task_group")) 60 | total, _ := query.Count() 61 | query.OrderBy("-id").Limit(pageSize, offset).All(&list) 62 | 63 | return list, total 64 | } 65 | -------------------------------------------------------------------------------- /app/models/task_log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/astaxie/beego/orm" 5 | ) 6 | 7 | type TaskLog struct { 8 | Id int 9 | TaskId int 10 | Output string 11 | Error string 12 | Status int 13 | ProcessTime int 14 | CreateTime int64 15 | } 16 | 17 | func (t *TaskLog) TableName() string { 18 | return TableName("task_log") 19 | } 20 | 21 | func TaskLogAdd(t *TaskLog) (int64, error) { 22 | return orm.NewOrm().Insert(t) 23 | } 24 | 25 | func TaskLogGetList(page, pageSize int, filters ...interface{}) ([]*TaskLog, int64) { 26 | offset := (page - 1) * pageSize 27 | 28 | logs := make([]*TaskLog, 0) 29 | 30 | query := orm.NewOrm().QueryTable(TableName("task_log")) 31 | if len(filters) > 0 { 32 | l := len(filters) 33 | for k := 0; k < l; k += 2 { 34 | query = query.Filter(filters[k].(string), filters[k+1]) 35 | } 36 | } 37 | 38 | total, _ := query.Count() 39 | query.OrderBy("-id").Limit(pageSize, offset).All(&logs) 40 | 41 | return logs, total 42 | } 43 | 44 | func TaskLogGetById(id int) (*TaskLog, error) { 45 | obj := &TaskLog{ 46 | Id: id, 47 | } 48 | 49 | err := orm.NewOrm().Read(obj) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return obj, nil 54 | } 55 | 56 | func TaskLogDelById(id int) error { 57 | _, err := orm.NewOrm().QueryTable(TableName("task_log")).Filter("id", id).Delete() 58 | return err 59 | } 60 | 61 | func TaskLogDelByTaskId(taskId int) (int64, error) { 62 | return orm.NewOrm().QueryTable(TableName("task_log")).Filter("task_id", taskId).Delete() 63 | } 64 | -------------------------------------------------------------------------------- /app/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/astaxie/beego/orm" 5 | ) 6 | 7 | type User struct { 8 | Id int 9 | UserName string 10 | Password string 11 | Salt string 12 | Email string 13 | LastLogin int64 14 | LastIp string 15 | Status int 16 | } 17 | 18 | func (u *User) TableName() string { 19 | return TableName("user") 20 | } 21 | 22 | func (u *User) Update(fields ...string) error { 23 | if _, err := orm.NewOrm().Update(u, fields...); err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func UserAdd(user *User) (int64, error) { 30 | return orm.NewOrm().Insert(user) 31 | } 32 | 33 | func UserGetById(id int) (*User, error) { 34 | u := new(User) 35 | 36 | err := orm.NewOrm().QueryTable(TableName("user")).Filter("id", id).One(u) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return u, nil 41 | } 42 | 43 | func UserGetByName(userName string) (*User, error) { 44 | u := new(User) 45 | 46 | err := orm.NewOrm().QueryTable(TableName("user")).Filter("user_name", userName).One(u) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return u, nil 51 | } 52 | 53 | func UserUpdate(user *User, fields ...string) error { 54 | _, err := orm.NewOrm().Update(user, fields...) 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /conf/app.conf: -------------------------------------------------------------------------------- 1 | appname = webcron 2 | httpport = 8000 3 | runmode = dev 4 | 5 | # 允许同时运行的任务数 6 | jobs.pool = 10 7 | 8 | # 站点名称 9 | site.name = 定时任务管理器 10 | 11 | # 数据库配置 12 | db.host = 127.0.0.1 13 | db.user = root 14 | db.password = "" 15 | db.port = 3306 16 | db.name = webcron 17 | db.prefix = t_ 18 | db.timezone = Asia/Shanghai 19 | 20 | # 邮件服务器配置 21 | mail.queue_size = 100 22 | mail.from = no-reply@example.com 23 | mail.host = smtp.example.com 24 | mail.port = 25 25 | mail.user = username 26 | mail.password = your password 27 | 28 | -------------------------------------------------------------------------------- /install.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `t_task` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID', 4 | `group_id` int(11) NOT NULL DEFAULT '0' COMMENT '分组ID', 5 | `task_name` varchar(50) NOT NULL DEFAULT '' COMMENT '任务名称', 6 | `task_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '任务类型', 7 | `description` varchar(200) NOT NULL DEFAULT '' COMMENT '任务描述', 8 | `cron_spec` varchar(100) NOT NULL DEFAULT '' COMMENT '时间表达式', 9 | `concurrent` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否只允许一个实例', 10 | `command` text NOT NULL COMMENT '命令详情', 11 | `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0停用 1启用', 12 | `notify` tinyint(4) NOT NULL DEFAULT '0' COMMENT '通知设置', 13 | `notify_email` text NOT NULL COMMENT '通知人列表', 14 | `timeout` smallint(6) NOT NULL DEFAULT '0' COMMENT '超时设置', 15 | `execute_times` int(11) NOT NULL DEFAULT '0' COMMENT '累计执行次数', 16 | `prev_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上次执行时间', 17 | `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 18 | PRIMARY KEY (`id`), 19 | KEY `idx_user_id` (`user_id`), 20 | KEY `idx_group_id` (`group_id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 22 | 23 | CREATE TABLE `t_task_group` ( 24 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 25 | `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID', 26 | `group_name` varchar(50) NOT NULL DEFAULT '' COMMENT '组名', 27 | `description` varchar(255) NOT NULL DEFAULT '' COMMENT '说明', 28 | `create_time` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 29 | PRIMARY KEY (`id`), 30 | KEY `idx_user_id` (`user_id`) 31 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 32 | 33 | 34 | CREATE TABLE `t_task_log` ( 35 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 36 | `task_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '任务ID', 37 | `output` mediumtext NOT NULL COMMENT '任务输出', 38 | `error` text NOT NULL COMMENT '错误信息', 39 | `status` tinyint(4) NOT NULL COMMENT '状态', 40 | `process_time` int(11) NOT NULL DEFAULT '0' COMMENT '消耗时间/毫秒', 41 | `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 42 | PRIMARY KEY (`id`), 43 | KEY `idx_task_id` (`task_id`,`create_time`) 44 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 45 | 46 | 47 | CREATE TABLE `t_user` ( 48 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 49 | `user_name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', 50 | `email` varchar(50) NOT NULL DEFAULT '' COMMENT '邮箱', 51 | `password` char(32) NOT NULL DEFAULT '' COMMENT '密码', 52 | `salt` char(10) NOT NULL DEFAULT '' COMMENT '密码盐', 53 | `last_login` int(11) NOT NULL DEFAULT '0' COMMENT '最后登录时间', 54 | `last_ip` char(15) NOT NULL DEFAULT '' COMMENT '最后登录IP', 55 | `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态,0正常 -1禁用', 56 | PRIMARY KEY (`id`), 57 | UNIQUE KEY `idx_user_name` (`user_name`) 58 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 59 | 60 | INSERT INTO `t_user` (`id`, `user_name`, `email`, `password`, `salt`, `last_login`, `last_ip`, `status`) 61 | VALUES (1,'admin','admin@example.com','7fef6171469e80d32c0559f88b377245','',0,'',0); -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/lisijie/webcron/app/controllers" 6 | "github.com/lisijie/webcron/app/jobs" 7 | _ "github.com/lisijie/webcron/app/mail" 8 | "github.com/lisijie/webcron/app/models" 9 | "html/template" 10 | "net/http" 11 | ) 12 | 13 | const VERSION = "1.0.0" 14 | 15 | func main() { 16 | models.Init() 17 | jobs.InitJobs() 18 | 19 | // 设置默认404页面 20 | beego.ErrorHandler("404", func(rw http.ResponseWriter, r *http.Request) { 21 | t, _ := template.New("404.html").ParseFiles(beego.BConfig.WebConfig.ViewsPath + "/error/404.html") 22 | data := make(map[string]interface{}) 23 | data["content"] = "page not found" 24 | t.Execute(rw, data) 25 | }) 26 | 27 | // 生产环境不输出debug日志 28 | if beego.AppConfig.String("runmode") == "prod" { 29 | beego.SetLevel(beego.LevelInformational) 30 | } 31 | beego.AppConfig.Set("version", VERSION) 32 | 33 | // 路由设置 34 | beego.Router("/", &controllers.MainController{}, "*:Index") 35 | beego.Router("/login", &controllers.MainController{}, "*:Login") 36 | beego.Router("/logout", &controllers.MainController{}, "*:Logout") 37 | beego.Router("/profile", &controllers.MainController{}, "*:Profile") 38 | beego.Router("/gettime", &controllers.MainController{}, "*:GetTime") 39 | beego.Router("/help", &controllers.HelpController{}, "*:Index") 40 | beego.AutoRouter(&controllers.TaskController{}) 41 | beego.AutoRouter(&controllers.GroupController{}) 42 | 43 | beego.BConfig.WebConfig.Session.SessionOn = true 44 | beego.Run() 45 | } 46 | -------------------------------------------------------------------------------- /pack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | tarfile="webcron-$1.tar.gz" 4 | 5 | echo "开始打包$tarfile..." 6 | 7 | export GOARCH=amd64 8 | export GOOS=linux 9 | 10 | bee pack 11 | 12 | mv webcron.tar.gz $tarfile 13 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case $1 in 4 | start) 5 | nohup ./webcron 2>&1 >> info.log 2>&1 /dev/null & 6 | echo "服务已启动..." 7 | sleep 1 8 | ;; 9 | stop) 10 | killall webcron 11 | echo "服务已停止..." 12 | sleep 1 13 | ;; 14 | restart) 15 | killall webcron 16 | sleep 1 17 | nohup ./webcron 2>&1 >> info.log 2>&1 /dev/null & 18 | echo "服务已重启..." 19 | sleep 1 20 | ;; 21 | *) 22 | echo "$0 {start|stop|restart}" 23 | exit 4 24 | ;; 25 | esac 26 | 27 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/screenshot.png -------------------------------------------------------------------------------- /static/css/base-admin-responsive.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} 2 | .clearfix:after{clear:both;} 3 | .hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;} 4 | .input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} 5 | .hidden{display:none;visibility:hidden;} 6 | .visible-phone{display:none;} 7 | .visible-tablet{display:none;} 8 | .visible-desktop{display:block;} 9 | .hidden-phone{display:block;} 10 | .hidden-tablet{display:block;} 11 | .hidden-desktop{display:none;} 12 | @media (max-width:767px){.visible-phone{display:block;} .hidden-phone{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (min-width:768px) and (max-width:979px){.visible-tablet{display:block;} .hidden-tablet{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:18px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top{margin-left:-20px;margin-right:-20px;} .container{width:auto;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;} .thumbnails [class*="span"]{width:auto;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:99.999999993%;} .row-fluid > .span11{width:91.436464082%;} .row-fluid > .span10{width:82.87292817100001%;} .row-fluid > .span9{width:74.30939226%;} .row-fluid > .span8{width:65.74585634900001%;} .row-fluid > .span7{width:57.182320438000005%;} .row-fluid > .span6{width:48.618784527%;} .row-fluid > .span5{width:40.055248616%;} .row-fluid > .span4{width:31.491712705%;} .row-fluid > .span3{width:22.928176794%;} .row-fluid > .span2{width:14.364640883%;} .row-fluid > .span1{width:5.801104972%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:714px;} input.span11, textarea.span11, .uneditable-input.span11{width:652px;} input.span10, textarea.span10, .uneditable-input.span10{width:590px;} input.span9, textarea.span9, .uneditable-input.span9{width:528px;} input.span8, textarea.span8, .uneditable-input.span8{width:466px;} input.span7, textarea.span7, .uneditable-input.span7{width:404px;} input.span6, textarea.span6, .uneditable-input.span6{width:342px;} input.span5, textarea.span5, .uneditable-input.span5{width:280px;} input.span4, textarea.span4, .uneditable-input.span4{width:218px;} input.span3, textarea.span3, .uneditable-input.span3{width:156px;} input.span2, textarea.span2, .uneditable-input.span2{width:94px;} input.span1, textarea.span1, .uneditable-input.span1{width:32px;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:18px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 9px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav .nav-header{color:#999999;text-shadow:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:100%;} .row-fluid > .span11{width:91.45299145300001%;} .row-fluid > .span10{width:82.905982906%;} .row-fluid > .span9{width:74.358974359%;} .row-fluid > .span8{width:65.81196581200001%;} .row-fluid > .span7{width:57.264957265%;} .row-fluid > .span6{width:48.717948718%;} .row-fluid > .span5{width:40.170940171000005%;} .row-fluid > .span4{width:31.623931624%;} .row-fluid > .span3{width:23.076923077%;} .row-fluid > .span2{width:14.529914530000001%;} .row-fluid > .span1{width:5.982905983%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:1160px;} input.span11, textarea.span11, .uneditable-input.span11{width:1060px;} input.span10, textarea.span10, .uneditable-input.span10{width:960px;} input.span9, textarea.span9, .uneditable-input.span9{width:860px;} input.span8, textarea.span8, .uneditable-input.span8{width:760px;} input.span7, textarea.span7, .uneditable-input.span7{width:660px;} input.span6, textarea.span6, .uneditable-input.span6{width:560px;} input.span5, textarea.span5, .uneditable-input.span5{width:460px;} input.span4, textarea.span4, .uneditable-input.span4{width:360px;} input.span3, textarea.span3, .uneditable-input.span3{width:260px;} input.span2, textarea.span2, .uneditable-input.span2{width:160px;} input.span1, textarea.span1, .uneditable-input.span1{width:60px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}} 13 | -------------------------------------------------------------------------------- /static/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'FontAwesome';src:url('../font/fontawesome-webfont.eot?v=3.2.1');src:url('../font/fontawesome-webfont.eot?#iefix&v=3.2.1') format('embedded-opentype'),url('../font/fontawesome-webfont.woff?v=3.2.1') format('woff'),url('../font/fontawesome-webfont.ttf?v=3.2.1') format('truetype'),url('../font/fontawesome-webfont.svg#fontawesomeregular?v=3.2.1') format('svg');font-weight:normal;font-style:normal;}[class^="icon-"],[class*=" icon-"]{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em;} 2 | [class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none;} 3 | .icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em;} 4 | a [class^="icon-"],a [class*=" icon-"]{display:inline;} 5 | [class^="icon-"].icon-fixed-width,[class*=" icon-"].icon-fixed-width{display:inline-block;width:1.1428571428571428em;text-align:right;padding-right:0.2857142857142857em;}[class^="icon-"].icon-fixed-width.icon-large,[class*=" icon-"].icon-fixed-width.icon-large{width:1.4285714285714286em;} 6 | .icons-ul{margin-left:2.142857142857143em;list-style-type:none;}.icons-ul>li{position:relative;} 7 | .icons-ul .icon-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;text-align:center;line-height:inherit;} 8 | [class^="icon-"].hide,[class*=" icon-"].hide{display:none;} 9 | .icon-muted{color:#eeeeee;} 10 | .icon-light{color:#ffffff;} 11 | .icon-dark{color:#333333;} 12 | .icon-border{border:solid 1px #eeeeee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} 13 | .icon-2x{font-size:2em;}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} 14 | .icon-3x{font-size:3em;}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} 15 | .icon-4x{font-size:4em;}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} 16 | .icon-5x{font-size:5em;}.icon-5x.icon-border{border-width:5px;-webkit-border-radius:7px;-moz-border-radius:7px;border-radius:7px;} 17 | .pull-right{float:right;} 18 | .pull-left{float:left;} 19 | [class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.3em;} 20 | [class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.3em;} 21 | [class^="icon-"],[class*=" icon-"]{display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0% 0%;background-repeat:repeat;margin-top:0;} 22 | .icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:none;} 23 | .btn [class^="icon-"].icon-large,.nav [class^="icon-"].icon-large,.btn [class*=" icon-"].icon-large,.nav [class*=" icon-"].icon-large{line-height:.9em;} 24 | .btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block;} 25 | .nav-tabs [class^="icon-"],.nav-pills [class^="icon-"],.nav-tabs [class*=" icon-"],.nav-pills [class*=" icon-"],.nav-tabs [class^="icon-"].icon-large,.nav-pills [class^="icon-"].icon-large,.nav-tabs [class*=" icon-"].icon-large,.nav-pills [class*=" icon-"].icon-large{line-height:.9em;} 26 | .btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.18em;} 27 | .btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{line-height:.8em;} 28 | .btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.25em;} 29 | .btn.btn-large [class^="icon-"],.btn.btn-large [class*=" icon-"]{margin-top:0;}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.05em;} 30 | .btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x{margin-right:.2em;} 31 | .btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-left:.2em;} 32 | .nav-list [class^="icon-"],.nav-list [class*=" icon-"]{line-height:inherit;} 33 | .icon-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:-35%;}.icon-stack [class^="icon-"],.icon-stack [class*=" icon-"]{display:block;text-align:center;position:absolute;width:100%;height:100%;font-size:1em;line-height:inherit;*line-height:2em;} 34 | .icon-stack .icon-stack-base{font-size:2em;*line-height:1em;} 35 | .icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear;} 36 | a .icon-stack,a .icon-spin{display:inline-block;text-decoration:none;} 37 | @-moz-keyframes spin{0%{-moz-transform:rotate(0deg);} 100%{-moz-transform:rotate(359deg);}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);} 100%{-webkit-transform:rotate(359deg);}}@-o-keyframes spin{0%{-o-transform:rotate(0deg);} 100%{-o-transform:rotate(359deg);}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg);} 100%{-ms-transform:rotate(359deg);}}@keyframes spin{0%{transform:rotate(0deg);} 100%{transform:rotate(359deg);}}.icon-rotate-90:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);} 38 | .icon-rotate-180:before{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);} 39 | .icon-rotate-270:before{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);} 40 | .icon-flip-horizontal:before{-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1);} 41 | .icon-flip-vertical:before{-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1);} 42 | a .icon-rotate-90:before,a .icon-rotate-180:before,a .icon-rotate-270:before,a .icon-flip-horizontal:before,a .icon-flip-vertical:before{display:inline-block;} 43 | .icon-glass:before{content:"\f000";} 44 | .icon-music:before{content:"\f001";} 45 | .icon-search:before{content:"\f002";} 46 | .icon-envelope-alt:before{content:"\f003";} 47 | .icon-heart:before{content:"\f004";} 48 | .icon-star:before{content:"\f005";} 49 | .icon-star-empty:before{content:"\f006";} 50 | .icon-user:before{content:"\f007";} 51 | .icon-film:before{content:"\f008";} 52 | .icon-th-large:before{content:"\f009";} 53 | .icon-th:before{content:"\f00a";} 54 | .icon-th-list:before{content:"\f00b";} 55 | .icon-ok:before{content:"\f00c";} 56 | .icon-remove:before{content:"\f00d";} 57 | .icon-zoom-in:before{content:"\f00e";} 58 | .icon-zoom-out:before{content:"\f010";} 59 | .icon-power-off:before,.icon-off:before{content:"\f011";} 60 | .icon-signal:before{content:"\f012";} 61 | .icon-gear:before,.icon-cog:before{content:"\f013";} 62 | .icon-trash:before{content:"\f014";} 63 | .icon-home:before{content:"\f015";} 64 | .icon-file-alt:before{content:"\f016";} 65 | .icon-time:before{content:"\f017";} 66 | .icon-road:before{content:"\f018";} 67 | .icon-download-alt:before{content:"\f019";} 68 | .icon-download:before{content:"\f01a";} 69 | .icon-upload:before{content:"\f01b";} 70 | .icon-inbox:before{content:"\f01c";} 71 | .icon-play-circle:before{content:"\f01d";} 72 | .icon-rotate-right:before,.icon-repeat:before{content:"\f01e";} 73 | .icon-refresh:before{content:"\f021";} 74 | .icon-list-alt:before{content:"\f022";} 75 | .icon-lock:before{content:"\f023";} 76 | .icon-flag:before{content:"\f024";} 77 | .icon-headphones:before{content:"\f025";} 78 | .icon-volume-off:before{content:"\f026";} 79 | .icon-volume-down:before{content:"\f027";} 80 | .icon-volume-up:before{content:"\f028";} 81 | .icon-qrcode:before{content:"\f029";} 82 | .icon-barcode:before{content:"\f02a";} 83 | .icon-tag:before{content:"\f02b";} 84 | .icon-tags:before{content:"\f02c";} 85 | .icon-book:before{content:"\f02d";} 86 | .icon-bookmark:before{content:"\f02e";} 87 | .icon-print:before{content:"\f02f";} 88 | .icon-camera:before{content:"\f030";} 89 | .icon-font:before{content:"\f031";} 90 | .icon-bold:before{content:"\f032";} 91 | .icon-italic:before{content:"\f033";} 92 | .icon-text-height:before{content:"\f034";} 93 | .icon-text-width:before{content:"\f035";} 94 | .icon-align-left:before{content:"\f036";} 95 | .icon-align-center:before{content:"\f037";} 96 | .icon-align-right:before{content:"\f038";} 97 | .icon-align-justify:before{content:"\f039";} 98 | .icon-list:before{content:"\f03a";} 99 | .icon-indent-left:before{content:"\f03b";} 100 | .icon-indent-right:before{content:"\f03c";} 101 | .icon-facetime-video:before{content:"\f03d";} 102 | .icon-picture:before{content:"\f03e";} 103 | .icon-pencil:before{content:"\f040";} 104 | .icon-map-marker:before{content:"\f041";} 105 | .icon-adjust:before{content:"\f042";} 106 | .icon-tint:before{content:"\f043";} 107 | .icon-edit:before{content:"\f044";} 108 | .icon-share:before{content:"\f045";} 109 | .icon-check:before{content:"\f046";} 110 | .icon-move:before{content:"\f047";} 111 | .icon-step-backward:before{content:"\f048";} 112 | .icon-fast-backward:before{content:"\f049";} 113 | .icon-backward:before{content:"\f04a";} 114 | .icon-play:before{content:"\f04b";} 115 | .icon-pause:before{content:"\f04c";} 116 | .icon-stop:before{content:"\f04d";} 117 | .icon-forward:before{content:"\f04e";} 118 | .icon-fast-forward:before{content:"\f050";} 119 | .icon-step-forward:before{content:"\f051";} 120 | .icon-eject:before{content:"\f052";} 121 | .icon-chevron-left:before{content:"\f053";} 122 | .icon-chevron-right:before{content:"\f054";} 123 | .icon-plus-sign:before{content:"\f055";} 124 | .icon-minus-sign:before{content:"\f056";} 125 | .icon-remove-sign:before{content:"\f057";} 126 | .icon-ok-sign:before{content:"\f058";} 127 | .icon-question-sign:before{content:"\f059";} 128 | .icon-info-sign:before{content:"\f05a";} 129 | .icon-screenshot:before{content:"\f05b";} 130 | .icon-remove-circle:before{content:"\f05c";} 131 | .icon-ok-circle:before{content:"\f05d";} 132 | .icon-ban-circle:before{content:"\f05e";} 133 | .icon-arrow-left:before{content:"\f060";} 134 | .icon-arrow-right:before{content:"\f061";} 135 | .icon-arrow-up:before{content:"\f062";} 136 | .icon-arrow-down:before{content:"\f063";} 137 | .icon-mail-forward:before,.icon-share-alt:before{content:"\f064";} 138 | .icon-resize-full:before{content:"\f065";} 139 | .icon-resize-small:before{content:"\f066";} 140 | .icon-plus:before{content:"\f067";} 141 | .icon-minus:before{content:"\f068";} 142 | .icon-asterisk:before{content:"\f069";} 143 | .icon-exclamation-sign:before{content:"\f06a";} 144 | .icon-gift:before{content:"\f06b";} 145 | .icon-leaf:before{content:"\f06c";} 146 | .icon-fire:before{content:"\f06d";} 147 | .icon-eye-open:before{content:"\f06e";} 148 | .icon-eye-close:before{content:"\f070";} 149 | .icon-warning-sign:before{content:"\f071";} 150 | .icon-plane:before{content:"\f072";} 151 | .icon-calendar:before{content:"\f073";} 152 | .icon-random:before{content:"\f074";} 153 | .icon-comment:before{content:"\f075";} 154 | .icon-magnet:before{content:"\f076";} 155 | .icon-chevron-up:before{content:"\f077";} 156 | .icon-chevron-down:before{content:"\f078";} 157 | .icon-retweet:before{content:"\f079";} 158 | .icon-shopping-cart:before{content:"\f07a";} 159 | .icon-folder-close:before{content:"\f07b";} 160 | .icon-folder-open:before{content:"\f07c";} 161 | .icon-resize-vertical:before{content:"\f07d";} 162 | .icon-resize-horizontal:before{content:"\f07e";} 163 | .icon-bar-chart:before{content:"\f080";} 164 | .icon-twitter-sign:before{content:"\f081";} 165 | .icon-facebook-sign:before{content:"\f082";} 166 | .icon-camera-retro:before{content:"\f083";} 167 | .icon-key:before{content:"\f084";} 168 | .icon-gears:before,.icon-cogs:before{content:"\f085";} 169 | .icon-comments:before{content:"\f086";} 170 | .icon-thumbs-up-alt:before{content:"\f087";} 171 | .icon-thumbs-down-alt:before{content:"\f088";} 172 | .icon-star-half:before{content:"\f089";} 173 | .icon-heart-empty:before{content:"\f08a";} 174 | .icon-signout:before{content:"\f08b";} 175 | .icon-linkedin-sign:before{content:"\f08c";} 176 | .icon-pushpin:before{content:"\f08d";} 177 | .icon-external-link:before{content:"\f08e";} 178 | .icon-signin:before{content:"\f090";} 179 | .icon-trophy:before{content:"\f091";} 180 | .icon-github-sign:before{content:"\f092";} 181 | .icon-upload-alt:before{content:"\f093";} 182 | .icon-lemon:before{content:"\f094";} 183 | .icon-phone:before{content:"\f095";} 184 | .icon-unchecked:before,.icon-check-empty:before{content:"\f096";} 185 | .icon-bookmark-empty:before{content:"\f097";} 186 | .icon-phone-sign:before{content:"\f098";} 187 | .icon-twitter:before{content:"\f099";} 188 | .icon-facebook:before{content:"\f09a";} 189 | .icon-github:before{content:"\f09b";} 190 | .icon-unlock:before{content:"\f09c";} 191 | .icon-credit-card:before{content:"\f09d";} 192 | .icon-rss:before{content:"\f09e";} 193 | .icon-hdd:before{content:"\f0a0";} 194 | .icon-bullhorn:before{content:"\f0a1";} 195 | .icon-bell:before{content:"\f0a2";} 196 | .icon-certificate:before{content:"\f0a3";} 197 | .icon-hand-right:before{content:"\f0a4";} 198 | .icon-hand-left:before{content:"\f0a5";} 199 | .icon-hand-up:before{content:"\f0a6";} 200 | .icon-hand-down:before{content:"\f0a7";} 201 | .icon-circle-arrow-left:before{content:"\f0a8";} 202 | .icon-circle-arrow-right:before{content:"\f0a9";} 203 | .icon-circle-arrow-up:before{content:"\f0aa";} 204 | .icon-circle-arrow-down:before{content:"\f0ab";} 205 | .icon-globe:before{content:"\f0ac";} 206 | .icon-wrench:before{content:"\f0ad";} 207 | .icon-tasks:before{content:"\f0ae";} 208 | .icon-filter:before{content:"\f0b0";} 209 | .icon-briefcase:before{content:"\f0b1";} 210 | .icon-fullscreen:before{content:"\f0b2";} 211 | .icon-group:before{content:"\f0c0";} 212 | .icon-link:before{content:"\f0c1";} 213 | .icon-cloud:before{content:"\f0c2";} 214 | .icon-beaker:before{content:"\f0c3";} 215 | .icon-cut:before{content:"\f0c4";} 216 | .icon-copy:before{content:"\f0c5";} 217 | .icon-paperclip:before,.icon-paper-clip:before{content:"\f0c6";} 218 | .icon-save:before{content:"\f0c7";} 219 | .icon-sign-blank:before{content:"\f0c8";} 220 | .icon-reorder:before{content:"\f0c9";} 221 | .icon-list-ul:before{content:"\f0ca";} 222 | .icon-list-ol:before{content:"\f0cb";} 223 | .icon-strikethrough:before{content:"\f0cc";} 224 | .icon-underline:before{content:"\f0cd";} 225 | .icon-table:before{content:"\f0ce";} 226 | .icon-magic:before{content:"\f0d0";} 227 | .icon-truck:before{content:"\f0d1";} 228 | .icon-pinterest:before{content:"\f0d2";} 229 | .icon-pinterest-sign:before{content:"\f0d3";} 230 | .icon-google-plus-sign:before{content:"\f0d4";} 231 | .icon-google-plus:before{content:"\f0d5";} 232 | .icon-money:before{content:"\f0d6";} 233 | .icon-caret-down:before{content:"\f0d7";} 234 | .icon-caret-up:before{content:"\f0d8";} 235 | .icon-caret-left:before{content:"\f0d9";} 236 | .icon-caret-right:before{content:"\f0da";} 237 | .icon-columns:before{content:"\f0db";} 238 | .icon-sort:before{content:"\f0dc";} 239 | .icon-sort-down:before{content:"\f0dd";} 240 | .icon-sort-up:before{content:"\f0de";} 241 | .icon-envelope:before{content:"\f0e0";} 242 | .icon-linkedin:before{content:"\f0e1";} 243 | .icon-rotate-left:before,.icon-undo:before{content:"\f0e2";} 244 | .icon-legal:before{content:"\f0e3";} 245 | .icon-dashboard:before{content:"\f0e4";} 246 | .icon-comment-alt:before{content:"\f0e5";} 247 | .icon-comments-alt:before{content:"\f0e6";} 248 | .icon-bolt:before{content:"\f0e7";} 249 | .icon-sitemap:before{content:"\f0e8";} 250 | .icon-umbrella:before{content:"\f0e9";} 251 | .icon-paste:before{content:"\f0ea";} 252 | .icon-lightbulb:before{content:"\f0eb";} 253 | .icon-exchange:before{content:"\f0ec";} 254 | .icon-cloud-download:before{content:"\f0ed";} 255 | .icon-cloud-upload:before{content:"\f0ee";} 256 | .icon-user-md:before{content:"\f0f0";} 257 | .icon-stethoscope:before{content:"\f0f1";} 258 | .icon-suitcase:before{content:"\f0f2";} 259 | .icon-bell-alt:before{content:"\f0f3";} 260 | .icon-coffee:before{content:"\f0f4";} 261 | .icon-food:before{content:"\f0f5";} 262 | .icon-file-text-alt:before{content:"\f0f6";} 263 | .icon-building:before{content:"\f0f7";} 264 | .icon-hospital:before{content:"\f0f8";} 265 | .icon-ambulance:before{content:"\f0f9";} 266 | .icon-medkit:before{content:"\f0fa";} 267 | .icon-fighter-jet:before{content:"\f0fb";} 268 | .icon-beer:before{content:"\f0fc";} 269 | .icon-h-sign:before{content:"\f0fd";} 270 | .icon-plus-sign-alt:before{content:"\f0fe";} 271 | .icon-double-angle-left:before{content:"\f100";} 272 | .icon-double-angle-right:before{content:"\f101";} 273 | .icon-double-angle-up:before{content:"\f102";} 274 | .icon-double-angle-down:before{content:"\f103";} 275 | .icon-angle-left:before{content:"\f104";} 276 | .icon-angle-right:before{content:"\f105";} 277 | .icon-angle-up:before{content:"\f106";} 278 | .icon-angle-down:before{content:"\f107";} 279 | .icon-desktop:before{content:"\f108";} 280 | .icon-laptop:before{content:"\f109";} 281 | .icon-tablet:before{content:"\f10a";} 282 | .icon-mobile-phone:before{content:"\f10b";} 283 | .icon-circle-blank:before{content:"\f10c";} 284 | .icon-quote-left:before{content:"\f10d";} 285 | .icon-quote-right:before{content:"\f10e";} 286 | .icon-spinner:before{content:"\f110";} 287 | .icon-circle:before{content:"\f111";} 288 | .icon-mail-reply:before,.icon-reply:before{content:"\f112";} 289 | .icon-github-alt:before{content:"\f113";} 290 | .icon-folder-close-alt:before{content:"\f114";} 291 | .icon-folder-open-alt:before{content:"\f115";} 292 | .icon-expand-alt:before{content:"\f116";} 293 | .icon-collapse-alt:before{content:"\f117";} 294 | .icon-smile:before{content:"\f118";} 295 | .icon-frown:before{content:"\f119";} 296 | .icon-meh:before{content:"\f11a";} 297 | .icon-gamepad:before{content:"\f11b";} 298 | .icon-keyboard:before{content:"\f11c";} 299 | .icon-flag-alt:before{content:"\f11d";} 300 | .icon-flag-checkered:before{content:"\f11e";} 301 | .icon-terminal:before{content:"\f120";} 302 | .icon-code:before{content:"\f121";} 303 | .icon-reply-all:before{content:"\f122";} 304 | .icon-mail-reply-all:before{content:"\f122";} 305 | .icon-star-half-full:before,.icon-star-half-empty:before{content:"\f123";} 306 | .icon-location-arrow:before{content:"\f124";} 307 | .icon-crop:before{content:"\f125";} 308 | .icon-code-fork:before{content:"\f126";} 309 | .icon-unlink:before{content:"\f127";} 310 | .icon-question:before{content:"\f128";} 311 | .icon-info:before{content:"\f129";} 312 | .icon-exclamation:before{content:"\f12a";} 313 | .icon-superscript:before{content:"\f12b";} 314 | .icon-subscript:before{content:"\f12c";} 315 | .icon-eraser:before{content:"\f12d";} 316 | .icon-puzzle-piece:before{content:"\f12e";} 317 | .icon-microphone:before{content:"\f130";} 318 | .icon-microphone-off:before{content:"\f131";} 319 | .icon-shield:before{content:"\f132";} 320 | .icon-calendar-empty:before{content:"\f133";} 321 | .icon-fire-extinguisher:before{content:"\f134";} 322 | .icon-rocket:before{content:"\f135";} 323 | .icon-maxcdn:before{content:"\f136";} 324 | .icon-chevron-sign-left:before{content:"\f137";} 325 | .icon-chevron-sign-right:before{content:"\f138";} 326 | .icon-chevron-sign-up:before{content:"\f139";} 327 | .icon-chevron-sign-down:before{content:"\f13a";} 328 | .icon-html5:before{content:"\f13b";} 329 | .icon-css3:before{content:"\f13c";} 330 | .icon-anchor:before{content:"\f13d";} 331 | .icon-unlock-alt:before{content:"\f13e";} 332 | .icon-bullseye:before{content:"\f140";} 333 | .icon-ellipsis-horizontal:before{content:"\f141";} 334 | .icon-ellipsis-vertical:before{content:"\f142";} 335 | .icon-rss-sign:before{content:"\f143";} 336 | .icon-play-sign:before{content:"\f144";} 337 | .icon-ticket:before{content:"\f145";} 338 | .icon-minus-sign-alt:before{content:"\f146";} 339 | .icon-check-minus:before{content:"\f147";} 340 | .icon-level-up:before{content:"\f148";} 341 | .icon-level-down:before{content:"\f149";} 342 | .icon-check-sign:before{content:"\f14a";} 343 | .icon-edit-sign:before{content:"\f14b";} 344 | .icon-external-link-sign:before{content:"\f14c";} 345 | .icon-share-sign:before{content:"\f14d";} 346 | .icon-compass:before{content:"\f14e";} 347 | .icon-collapse:before{content:"\f150";} 348 | .icon-collapse-top:before{content:"\f151";} 349 | .icon-expand:before{content:"\f152";} 350 | .icon-euro:before,.icon-eur:before{content:"\f153";} 351 | .icon-gbp:before{content:"\f154";} 352 | .icon-dollar:before,.icon-usd:before{content:"\f155";} 353 | .icon-rupee:before,.icon-inr:before{content:"\f156";} 354 | .icon-yen:before,.icon-jpy:before{content:"\f157";} 355 | .icon-renminbi:before,.icon-cny:before{content:"\f158";} 356 | .icon-won:before,.icon-krw:before{content:"\f159";} 357 | .icon-bitcoin:before,.icon-btc:before{content:"\f15a";} 358 | .icon-file:before{content:"\f15b";} 359 | .icon-file-text:before{content:"\f15c";} 360 | .icon-sort-by-alphabet:before{content:"\f15d";} 361 | .icon-sort-by-alphabet-alt:before{content:"\f15e";} 362 | .icon-sort-by-attributes:before{content:"\f160";} 363 | .icon-sort-by-attributes-alt:before{content:"\f161";} 364 | .icon-sort-by-order:before{content:"\f162";} 365 | .icon-sort-by-order-alt:before{content:"\f163";} 366 | .icon-thumbs-up:before{content:"\f164";} 367 | .icon-thumbs-down:before{content:"\f165";} 368 | .icon-youtube-sign:before{content:"\f166";} 369 | .icon-youtube:before{content:"\f167";} 370 | .icon-xing:before{content:"\f168";} 371 | .icon-xing-sign:before{content:"\f169";} 372 | .icon-youtube-play:before{content:"\f16a";} 373 | .icon-dropbox:before{content:"\f16b";} 374 | .icon-stackexchange:before{content:"\f16c";} 375 | .icon-instagram:before{content:"\f16d";} 376 | .icon-flickr:before{content:"\f16e";} 377 | .icon-adn:before{content:"\f170";} 378 | .icon-bitbucket:before{content:"\f171";} 379 | .icon-bitbucket-sign:before{content:"\f172";} 380 | .icon-tumblr:before{content:"\f173";} 381 | .icon-tumblr-sign:before{content:"\f174";} 382 | .icon-long-arrow-down:before{content:"\f175";} 383 | .icon-long-arrow-up:before{content:"\f176";} 384 | .icon-long-arrow-left:before{content:"\f177";} 385 | .icon-long-arrow-right:before{content:"\f178";} 386 | .icon-apple:before{content:"\f179";} 387 | .icon-windows:before{content:"\f17a";} 388 | .icon-android:before{content:"\f17b";} 389 | .icon-linux:before{content:"\f17c";} 390 | .icon-dribbble:before{content:"\f17d";} 391 | .icon-skype:before{content:"\f17e";} 392 | .icon-foursquare:before{content:"\f180";} 393 | .icon-trello:before{content:"\f181";} 394 | .icon-female:before{content:"\f182";} 395 | .icon-male:before{content:"\f183";} 396 | .icon-gittip:before{content:"\f184";} 397 | .icon-sun:before{content:"\f185";} 398 | .icon-moon:before{content:"\f186";} 399 | .icon-archive:before{content:"\f187";} 400 | .icon-bug:before{content:"\f188";} 401 | .icon-vk:before{content:"\f189";} 402 | .icon-weibo:before{content:"\f18a";} 403 | .icon-renren:before{content:"\f18b";} 404 | -------------------------------------------------------------------------------- /static/css/pages/dashboard.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | /*------------------------------------------------------------------ 8 | [1. Shortcuts / .shortcuts] 9 | */ 10 | 11 | .shortcuts { 12 | text-align: center; 13 | } 14 | 15 | .shortcuts .shortcut { 16 | width: 22.50%; 17 | display: inline-block; 18 | padding: 12px 0; 19 | margin: 0 .9% 1em; 20 | vertical-align: top; 21 | 22 | text-decoration: none; 23 | 24 | background: #f9f6f1; 25 | 26 | border-radius: 5px; 27 | } 28 | 29 | .shortcuts .shortcut .shortcut-icon { 30 | margin-top: .25em; 31 | margin-bottom: .25em; 32 | 33 | font-size: 32px; 34 | color: #545454; 35 | } 36 | 37 | .shortcuts .shortcut:hover { 38 | background: #00ba8b; 39 | } 40 | 41 | .shortcuts .shortcut:hover span{ 42 | color: #fff; 43 | } 44 | 45 | .shortcuts .shortcut:hover .shortcut-icon { 46 | color: #fff; 47 | } 48 | 49 | .shortcuts .shortcut-label { 50 | display: block; 51 | 52 | font-weight: 400; 53 | color: #545454; 54 | } 55 | 56 | 57 | 58 | /*------------------------------------------------------------------ 59 | [2. Stats / .stats] 60 | */ 61 | 62 | .stats { 63 | width: 100%; 64 | display: table; 65 | padding: 0 0 0 10px; 66 | margin-top: .5em; 67 | margin-bottom: 1.9em; 68 | } 69 | 70 | .stats .stat { 71 | display: table-cell; 72 | width: 40%; 73 | vertical-align: top; 74 | 75 | font-size: 11px; 76 | font-weight: bold; 77 | color: #999; 78 | } 79 | 80 | .stat-value { 81 | display: block; 82 | margin-bottom: .55em; 83 | 84 | font-size: 30px; 85 | font-weight: bold; 86 | letter-spacing: -2px; 87 | color: #444; 88 | } 89 | 90 | .stat-time { 91 | text-align: center; 92 | padding-top: 1.5em; 93 | } 94 | 95 | .stat-time .stat-value { 96 | color: #19bc9c; 97 | font-size: 40px; 98 | } 99 | 100 | .stats #donut-chart { 101 | height: 100px; 102 | margin-left: -20px; 103 | } 104 | 105 | 106 | 107 | 108 | 109 | /*------------------------------------------------------------------ 110 | [3. News Item / .news-items] 111 | */ 112 | 113 | .news-items { 114 | margin: 1em 0 0; 115 | } 116 | 117 | .news-items li { 118 | display: table; 119 | padding: 0 2em 0 1.5em; 120 | padding-bottom: 1em; 121 | margin-bottom: 1em; 122 | border-bottom: 1px dotted #CCC; 123 | } 124 | 125 | .news-items li:last-child { padding-bottom: 0; border: none; } 126 | 127 | .news-item-date { 128 | display: table-cell; 129 | } 130 | 131 | .news-item-detail { 132 | display: table-cell; 133 | } 134 | 135 | .news-item-title { 136 | font-size: 13px; 137 | font-weight: 600; 138 | } 139 | 140 | .news-item-date { 141 | width: 75px; 142 | vertical-align: middle; 143 | text-align: center; 144 | 145 | } 146 | 147 | .news-item-day { 148 | display: block; 149 | margin-bottom: .25em; 150 | 151 | font-size: 24px; 152 | color: #888; 153 | } 154 | 155 | .news-item-preview { 156 | margin-bottom: 0; 157 | 158 | color: #777; 159 | } 160 | 161 | .news-item-month { 162 | display: block; 163 | padding-right: 1px; 164 | 165 | font-size: 12px; 166 | font-weight: 600; 167 | color: #888; 168 | } 169 | 170 | 171 | 172 | /*------------------------------------------------------------------ 173 | [4. Action Table / .action-table] 174 | */ 175 | 176 | .action-table .btn-small { 177 | padding: 4px 5px 5px; 178 | 179 | font-size: 10px; 180 | } 181 | 182 | .action-table .td-actions { 183 | width: 80px; 184 | 185 | text-align: center; 186 | } 187 | 188 | .action-table .td-actions .btn { 189 | margin-right: .5em; 190 | } 191 | 192 | .action-table .td-actions .btn:last-child { 193 | margin-rigth: 0; 194 | } 195 | 196 | 197 | 198 | #big_stats 199 | { 200 | width: 100%; 201 | display: table; 202 | margin-top: 1.5em; 203 | 204 | 205 | } 206 | 207 | .big-stats-container .widget-content { 208 | border:0; 209 | } 210 | 211 | #big_stats .stat 212 | { 213 | width: 25%; 214 | height: 90px; 215 | text-align: center; 216 | display: table-cell; 217 | padding: 0; 218 | position: relative; 219 | 220 | border-right: 1px solid #CCC; 221 | border-left: 1px solid #FFF; 222 | } 223 | #big_stats i { font-size:30px; display:block; line-height: 40px; color:#b2afaa;} 224 | #big_stats .stat:hover i {color:#19bc9c;} 225 | 226 | h6.bigstats{margin: 20px; 227 | border-bottom: 1px solid #eee; 228 | padding-bottom: 20px; 229 | margin-bottom: 26px;} 230 | 231 | #big_stats .stat:first-child { 232 | border-left: none; 233 | } 234 | 235 | #big_stats .stat:last-child { 236 | border-right: none; 237 | } 238 | 239 | #big_stats .stat h4 240 | { 241 | font-size: 11px; 242 | font-weight: bold; 243 | color: #777; 244 | margin-bottom: 1.5em; 245 | } 246 | 247 | #big_stats .stat .value 248 | { 249 | font-size: 45px; 250 | font-weight: bold; 251 | color: #545454; 252 | line-height: 1em; 253 | } 254 | 255 | 256 | 257 | @media all and (max-width: 950px) and (min-width: 1px) { 258 | 259 | #big_stats { 260 | display: block; 261 | margin-bottom: -40px; 262 | } 263 | 264 | #big_stats .stat { 265 | width: 49%; 266 | display: block; 267 | margin-bottom: 3em; 268 | float: left; 269 | } 270 | 271 | #big_stats .stat:nth-child(2) { 272 | border-right: none; 273 | } 274 | 275 | #big_stats .stat:nth-child(3) { 276 | border-left: none; 277 | } 278 | 279 | } 280 | 281 | @media (max-width: 767px) { 282 | #big_stats .stat .value { 283 | font-size: 40px; 284 | } 285 | } 286 | 287 | 288 | 289 | 290 | @media (max-width: 979px) { 291 | 292 | .shortcuts .shortcut { 293 | width: 31%; 294 | } 295 | } 296 | 297 | 298 | @media (max-width: 480px) { 299 | 300 | .stats .stat { 301 | 302 | margin-bottom: 3em; 303 | } 304 | 305 | .stats .stat .stat-value { 306 | margin-bottom: .15em; 307 | 308 | font-size: 20px; 309 | } 310 | 311 | .stats { 312 | float: left; 313 | 314 | display: block; 315 | 316 | margin-bottom: 0; 317 | } 318 | 319 | #chart-stats { 320 | margin: 2em 0 1em; 321 | } 322 | 323 | .shortcuts .shortcut { 324 | width: 48%; 325 | } 326 | } -------------------------------------------------------------------------------- /static/css/pages/faq.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | 8 | .faq-search { 9 | margin-bottom: 2em; 10 | 11 | text-align: right; 12 | } 13 | 14 | .faq-search input { 15 | width: 96%; 16 | display: block; 17 | padding: 2%; 18 | } 19 | 20 | 21 | 22 | .faq-empty { 23 | display: none; 24 | } 25 | 26 | 27 | 28 | .faq-toc { 29 | padding: 1.5em 0; 30 | margin: 2em 0 0; 31 | 32 | border: 1px dotted #CCC; 33 | border-right: none; 34 | border-left: none; 35 | } 36 | 37 | .faq-toc ol { 38 | padding: 0; 39 | margin: 0; 40 | } 41 | 42 | .faq-toc li { 43 | margin-bottom: .75em; 44 | 45 | list-style: none; 46 | } 47 | 48 | .faq-toc a { 49 | margin-left: .5em; 50 | } 51 | 52 | 53 | 54 | .faq-list { 55 | padding: 0; 56 | margin: 3em 0 0; 57 | 58 | list-style: none; 59 | } 60 | 61 | .faq-list li { 62 | display: table; 63 | margin-bottom: 2em; 64 | } 65 | 66 | .faq-icon { 67 | display: table-cell; 68 | padding-right: 1.25em; 69 | vertical-align: top; 70 | } 71 | 72 | .faq-text { 73 | display: table-cell; 74 | vertical-align: top; 75 | 76 | } 77 | 78 | 79 | .faq-number { 80 | width: 32px; 81 | height: 32px; 82 | 83 | font-size: 14px; 84 | font-weight: 600; 85 | text-align: center; 86 | line-height: 32px; 87 | color: #FFF; 88 | 89 | background: #00ba8b; 90 | 91 | border: 3px solid #FFF; 92 | 93 | 94 | 95 | border-radius: 100px; 96 | 97 | 98 | } 99 | 100 | 101 | 102 | 103 | .btn-support-ask { 104 | display: block; font-size: 22px; padding: 14px 0; font-weight: 600; margin-bottom: .75em; 105 | } 106 | 107 | .btn-support-contact { 108 | display: block; padding: 12px 0; font-size: 18px; font-weight: 600; 109 | } -------------------------------------------------------------------------------- /static/css/pages/plans.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | /*-- Plan Container --*/ 7 | 8 | .plan-container { 9 | position: relative; 10 | float: left; 11 | } 12 | 13 | /*-- Plan --*/ 14 | 15 | .plan { 16 | margin-right: 6px; 17 | 18 | 19 | } 20 | 21 | 22 | /*-- Plan Header --*/ 23 | 24 | .plan-header { 25 | text-align: center; 26 | color: #FFF; 27 | 28 | background-color: #686868; 29 | 30 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4); 31 | } 32 | 33 | 34 | .plan-title { 35 | padding: 10px 0; 36 | 37 | font-size: 16px; 38 | color: #FFF; 39 | 40 | border-bottom: 1px solid #FFF; 41 | border-bottom: 1px solid rgba(0, 0, 0, 0.3); 42 | 43 | 44 | } 45 | 46 | .plan-price { 47 | padding: 20px 0 10px; 48 | 49 | font-size: 66px; 50 | line-height: 0.8em; 51 | 52 | background-color: #797979; 53 | 54 | border-top: 1px solid rgba(255, 255, 255, 0.2); 55 | } 56 | 57 | .plan-price span.term { 58 | display: block; 59 | margin-bottom: 0; 60 | 61 | font-size: 13px; 62 | line-height: 0; 63 | padding: 2em 0 1em; 64 | } 65 | 66 | .plan-price span.note { 67 | position: relative; 68 | top: -40px; 69 | 70 | display: inline; 71 | 72 | font-size: 17px; 73 | line-height: 0.8em; 74 | } 75 | 76 | 77 | 78 | /*-- Plan Features --*/ 79 | 80 | .plan-features { 81 | border: 1px solid #DDD; 82 | border-bottom: none; 83 | } 84 | 85 | .plan-features { 86 | padding-bottom: 1em; 87 | } 88 | 89 | .plan-features ul { 90 | padding: 0; 91 | margin: 0; 92 | 93 | list-style: none; 94 | } 95 | 96 | .plan-features li { 97 | padding: 1em 0; 98 | margin: 0 2em; 99 | 100 | text-align: center; 101 | 102 | border-bottom: 1px dotted #CCC; 103 | } 104 | 105 | .plan-features li:last-child { 106 | border-bottom: none; 107 | } 108 | 109 | 110 | /*-- Plan Actions --*/ 111 | 112 | .plan-actions { 113 | padding: 1.15em 0; 114 | 115 | background: #F2F2F2; 116 | 117 | background-color: whiteSmoke; 118 | 119 | 120 | border: 1px solid #DDD; 121 | 122 | } 123 | 124 | .plan-actions .btn { 125 | padding: 1em 0; 126 | margin: 0 2em; 127 | 128 | display: block; 129 | 130 | font-size: 16px; 131 | font-weight: 600; 132 | } 133 | 134 | 135 | 136 | /*-- Columns --*/ 137 | 138 | .pricing-plans.plans-1 .plan-container { 139 | width: 100%; 140 | } 141 | .pricing-plans.plans-2 .plan-container { 142 | width: 50%; 143 | } 144 | 145 | .pricing-plans.plans-3 .plan-container { 146 | width: 33.33%; 147 | } 148 | 149 | .pricing-plans.plans-4 .plan-container { 150 | width: 25%; 151 | } 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | /*-- Best Value Highlight --*/ 161 | 162 | .plan.best-value .plan-header { 163 | background-color: #677E30; 164 | } 165 | 166 | .plan.best-value .plan-price { 167 | background-color: #81994D; 168 | } 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | .plan.skyblue .plan-header { 177 | background-color: #3D7AB8; 178 | } 179 | 180 | .plan.skyblue .plan-price { 181 | background-color: #69C; 182 | } 183 | 184 | 185 | 186 | .plan.lavendar .plan-header { 187 | background-color: #754F75; 188 | } 189 | 190 | .plan.lavendar .plan-price { 191 | background-color: #969; 192 | } 193 | 194 | 195 | 196 | .plan.teal .plan-header { 197 | background-color: #257272; 198 | } 199 | 200 | .plan.teal .plan-price { 201 | background-color: #399; 202 | } 203 | 204 | 205 | 206 | 207 | .plan.pink .plan-header { 208 | background-color: #FF3778; 209 | } 210 | 211 | .plan.pink .plan-price { 212 | background-color: #F69; 213 | } 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | .plan.black .plan-header { 222 | background-color: #222; 223 | } 224 | 225 | .plan.black .plan-price { 226 | background-color: #333; 227 | } 228 | 229 | 230 | 231 | 232 | 233 | .plan.yellow .plan-header { 234 | background-color: #C69E00; 235 | } 236 | 237 | .plan.yellow .plan-price { 238 | background-color: #E8B900; 239 | } 240 | 241 | 242 | 243 | .plan.purple .plan-header { 244 | background-color: #4E2675; 245 | } 246 | 247 | .plan.purple .plan-price { 248 | background-color: #639; 249 | } 250 | 251 | 252 | 253 | 254 | 255 | .plan.red .plan-header { 256 | background-color: #A40000; 257 | } 258 | 259 | .plan.red .plan-price { 260 | background-color: #C00; 261 | } 262 | 263 | 264 | 265 | .plan.orange .plan-header { 266 | background-color: #D98200; 267 | } 268 | 269 | .plan.orange .plan-price { 270 | background-color: #F90; 271 | } 272 | 273 | 274 | 275 | .plan.blue .plan-header { 276 | background-color: #0052A4; 277 | } 278 | 279 | .plan.blue .plan-price { 280 | background-color: #06C; 281 | } 282 | 283 | 284 | 285 | .plan.green .plan-header { 286 | background-color: #00ba8b ; 287 | } 288 | 289 | .plan.green .plan-price { 290 | background-color: #00ba8b ; 291 | } 292 | 293 | 294 | 295 | 296 | 297 | /*------------------------------------------------------------------ 298 | [2. Min Width: 767px / Max Width: 979px] 299 | */ 300 | 301 | @media (min-width: 767px) and (max-width: 979px) { 302 | 303 | .pricing-plans .plan-container { 304 | width: 50% !important; 305 | margin-bottom: 2em; 306 | } 307 | 308 | } 309 | 310 | 311 | 312 | @media (max-width: 767px) { 313 | 314 | .pricing-plans .plan-container { 315 | width: 100% !important; 316 | margin-bottom: 2em; 317 | } 318 | 319 | } -------------------------------------------------------------------------------- /static/css/pages/reports.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | .info-box { 6 | background:#ffffff; 7 | border:1px solid #c9c9c9; 8 | -webkit-border-radius: 3px; 9 | -moz-border-radius: 3px; 10 | border-radius: 3px; 11 | 12 | margin-bottom: 30px; 13 | } 14 | 15 | .stats-box { 16 | margin:40px 0px; 17 | color:#5f5f5f; 18 | } 19 | .stats-box-title { 20 | text-align:center; 21 | font-weight:bold; 22 | } 23 | .stats-box-all-info { 24 | text-align:center; 25 | font-weight:bold; 26 | font-size:48px; 27 | margin-top:20px; 28 | margin-bottom: 40px; 29 | } 30 | .stats-box-all-info i{ 31 | width:60px; 32 | height:60px; 33 | } -------------------------------------------------------------------------------- /static/css/pages/signin.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | 8 | /** Base Body Styles **/ 9 | body{ background: url(../../img/body-bg.png); color:#838383; font: 13px/1.7em 'Open Sans';} 10 | 11 | 12 | .account-container { 13 | width: 380px; 14 | display: block; 15 | margin: 60px auto 0 auto; 16 | 17 | background: #f9f9f9; 18 | 19 | border: 1px solid #d5d5d5; 20 | 21 | -webkit-border-radius: 5px; 22 | -moz-border-radius: 5px; 23 | border-radius: 5px; 24 | 25 | box-shadow: 0px 0px 2px #dadada, inset 0px -3px 0px #e6e6e6; 26 | } 27 | 28 | .content { 29 | padding: 16px 28px 23px; 30 | } 31 | 32 | .login-fields { 33 | 34 | } 35 | 36 | .login-fields .field { 37 | margin-bottom: 1.25em; 38 | } 39 | 40 | .login-fields label { 41 | display: none; 42 | } 43 | 44 | .login-fields input { 45 | font-family: 'Open Sans'; 46 | font-size: 13px; 47 | color: #8e8d8d; 48 | padding: 11px 15px 10px 50px; 49 | background-color: #fdfdfd; 50 | width: 255px; 51 | display: block; 52 | margin: 0; 53 | box-shadow: inset 2px 2px 4px #f1f1f1; 54 | } 55 | 56 | .username-field { background: url(../../img/signin/user.png) no-repeat; } 57 | 58 | .password-field { background: url(../../img/signin/password.png) no-repeat; } 59 | 60 | 61 | 62 | 63 | .login-actions { 64 | float: left; 65 | 66 | width: 100%; 67 | 68 | margin-top: -1em; 69 | margin-bottom: 1.25em; 70 | } 71 | 72 | .login-social { 73 | float: left; 74 | 75 | padding: 10px 0 15px; 76 | 77 | border: 1px dotted #CCC; 78 | border-right: none; 79 | border-left: none; 80 | } 81 | 82 | span.login-checkbox { 83 | float: left; 84 | margin-top: 31px; 85 | } 86 | 87 | span.login-checkbox > input[type='checkbox'] { 88 | opacity: 0; 89 | float: left; 90 | width: 15px; 91 | } 92 | 93 | span.login-checkbox > input[type='checkbox'] + label { 94 | clear: none; 95 | 96 | height: 15px; 97 | display: block; 98 | padding: 0 0 0 22px; 99 | margin: 0; 100 | 101 | font-size: 12px; 102 | line-height: 1.2em; 103 | 104 | background: url(../../img/signin/check.png) no-repeat 0 0; 105 | 106 | cursor: pointer; 107 | } 108 | 109 | span.login-checkbox > input[type='checkbox']:checked + label { 110 | background-position: 0 -15px; 111 | } 112 | 113 | /** Text Under Box**/ 114 | .login-extra { 115 | display: block; 116 | width: 300px; 117 | margin: 1.5em auto; 118 | 119 | text-align: left; 120 | line-height: 19px; 121 | 122 | text-shadow: 1px 1px 0px #fff; 123 | } 124 | 125 | 126 | .account-container h1 { 127 | margin-bottom: .4em; 128 | 129 | color: #525252; 130 | } 131 | 132 | /** Buttons **/ 133 | .twitter, .fb { 134 | position: relative; 135 | 136 | height: 32px; 137 | width: 157px; 138 | display: block; 139 | 140 | background: url(../../img/signin/twitter_btn.png) no-repeat; 141 | 142 | } 143 | 144 | .fb { 145 | width: 162px; 146 | 147 | background: url(../../img/signin/fb_btn.png) no-repeat; 148 | } 149 | 150 | .twitter:active, .fb:active { 151 | top: 1px; 152 | } 153 | 154 | .twitter:hover, .fb:hover { 155 | background-position: 0 -32px; 156 | } 157 | 158 | .twitter a, .fb a { 159 | padding: 5px 0 0 35px; 160 | text-shadow: -1px -1px 0px rgba(0,0,0,.3); 161 | color:#fff; 162 | font-weight: bold; 163 | font-size: 11px; 164 | height: 32px; 165 | display: block; 166 | } 167 | 168 | .fb a { 169 | padding: 5px 0 0 31px; 170 | 171 | } 172 | 173 | .twitter, .fb { 174 | display: inline-block; 175 | } 176 | 177 | .twitter a:hover, .fb a:hover { 178 | color: #FFF; 179 | text-decoration: none; 180 | } 181 | 182 | .button {-webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; display: inline-block; float: right; margin-top: 18px;} 183 | 184 | 185 | 186 | 187 | 188 | 189 | .register .login-social { 190 | margin-bottom: 1em; 191 | } 192 | 193 | .register .login-actions { 194 | margin-bottom: 0; 195 | } 196 | 197 | .register .login-fields input { 198 | width: 299px; 199 | padding-left: 6px; 200 | } 201 | 202 | .register h1 { 203 | color: #444; 204 | } 205 | 206 | .register span.login-checkbox { 207 | position: relative; 208 | top: -6px; 209 | 210 | width: 200px; 211 | } 212 | 213 | .register span.login-checkbox > input[type="checkbox"] + label { 214 | 215 | position: relative; 216 | 217 | line-height: 1.3em; 218 | } 219 | 220 | 221 | 222 | @media (max-width: 480px) { 223 | 224 | .account-container { 225 | width: 280px; 226 | margin-top: 35px; 227 | } 228 | 229 | .login-fields input { 230 | width: 160px; 231 | } 232 | 233 | .login-social { 234 | width: 100%; 235 | } 236 | 237 | .twitter { 238 | display: block; 239 | margin-bottom: 1em; 240 | } 241 | 242 | .register .login-fields input { 243 | width: 204px; 244 | padding-left: 6px; 245 | } 246 | 247 | } -------------------------------------------------------------------------------- /static/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/font/FontAwesome.otf -------------------------------------------------------------------------------- /static/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/font/fontawesome-webfont.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/font/fontawesome-webfont.svgz -------------------------------------------------------------------------------- /static/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/font/fontawesome-webfontd41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/font/fontawesome-webfontd41d.eot -------------------------------------------------------------------------------- /static/img/body-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/body-bg.png -------------------------------------------------------------------------------- /static/img/glyphicons-halflings-white.html: -------------------------------------------------------------------------------- 1 | 2 | 404 Not Found 3 | 4 |

404 Not Found

5 |
nginx
6 | 7 | 8 | -------------------------------------------------------------------------------- /static/img/glyphicons-halflings.html: -------------------------------------------------------------------------------- 1 | 2 | 404 Not Found 3 | 4 |

404 Not Found

5 |
nginx
6 | 7 | 8 | -------------------------------------------------------------------------------- /static/img/icons-sa7c41345d9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/icons-sa7c41345d9.png -------------------------------------------------------------------------------- /static/img/signin/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/signin/check.png -------------------------------------------------------------------------------- /static/img/signin/fb_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/signin/fb_btn.png -------------------------------------------------------------------------------- /static/img/signin/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/signin/password.png -------------------------------------------------------------------------------- /static/img/signin/twitter_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/signin/twitter_btn.png -------------------------------------------------------------------------------- /static/img/signin/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/img/signin/user.png -------------------------------------------------------------------------------- /static/js/base.js: -------------------------------------------------------------------------------- 1 | Date.prototype.Format = function (fmt) { 2 | var o = { 3 | "M+": this.getMonth() + 1, 4 | "d+": this.getDate(), 5 | "h+": this.getHours(), 6 | "m+": this.getMinutes(), 7 | "s+": this.getSeconds(), 8 | "q+": Math.floor((this.getMonth() + 3) / 3), 9 | "S": this.getMilliseconds() 10 | }; 11 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); 12 | for (var k in o) 13 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 14 | return fmt; 15 | } 16 | 17 | $(function () { 18 | $('.subnavbar').find ('li').each (function (i) { 19 | var mod = i % 3; 20 | if (mod === 2) { 21 | $(this).addClass ('subnavbar-open-right'); 22 | } 23 | }); 24 | initTime = new Date().getTime(); 25 | $.getJSON("/gettime", function(out) { 26 | setTime(initTime, out.time); 27 | }); 28 | }); 29 | 30 | function setTime(initTime,serverTime) { 31 | ellapsedTime = new Date().getTime()-initTime; 32 | $('#server-time').html('当前服务器时间: '+new Date(serverTime+ellapsedTime).Format("yyyy-MM-dd hh:mm:ss")+''); 33 | setTimeout('setTime('+initTime+','+serverTime+');',500); 34 | } -------------------------------------------------------------------------------- /static/js/chart.min.js: -------------------------------------------------------------------------------- 1 | var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= 2 | Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& 3 | isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? 4 | b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? 5 | 0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== 6 | a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* 8 | (a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* 9 | a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, 10 | scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", 11 | animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", 12 | scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, 13 | c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, 14 | onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, 15 | pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", 16 | scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); 17 | d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; 21 | h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< 35 | h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= 36 | Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0 0) 7 | data = data.slice(1); 8 | 9 | while (data.length < totalPoints) { 10 | var prev = data.length > 0 ? data[data.length - 1] : 50; 11 | var y = prev + Math.random() * 10 - 5; 12 | if (y < 0) 13 | y = 0; 14 | if (y > 100) 15 | y = 100; 16 | data.push(y); 17 | } 18 | 19 | var res = []; 20 | for (var i = 0; i < data.length; ++i) 21 | res.push([i, data[i]]) 22 | return res; 23 | } 24 | 25 | // setup plot 26 | var options = { 27 | yaxis: { min: 0, max: 100 }, 28 | xaxis: { min: 0, max: 100 }, 29 | colors: ["#F90", "#222", "#666", "#BBB"], 30 | series: { 31 | lines: { 32 | lineWidth: 2, 33 | fill: true, 34 | fillColor: { colors: [ { opacity: 0.6 }, { opacity: 0.2 } ] }, 35 | steps: false 36 | 37 | } 38 | } 39 | }; 40 | 41 | var plot = $.plot($("#area-chart"), [ getRandomData() ], options); 42 | }); -------------------------------------------------------------------------------- /static/js/charts/bar.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var data = new Array (); 3 | var ds = new Array(); 4 | 5 | data.push ([[1,25],[2,34],[3,37],[4,45],[5,56]]); 6 | data.push ([[1,13],[2,29],[3,25],[4,23],[5,31]]); 7 | data.push ([[1,8],[2,13],[3,19],[4,15],[5,14]]); 8 | data.push ([[1,20],[2,43],[3,29],[4,23],[5,25]]); 9 | 10 | for (var i=0, j=data.length; i'+label+'
'+Math.round(series.percent)+'%'; 20 | }, 21 | threshold: 0.1 22 | } 23 | } 24 | }, 25 | legend: { 26 | show: true, 27 | noColumns: 1, // number of colums in legend table 28 | labelFormatter: null, // fn: string -> string 29 | labelBoxBorderColor: "#888", // border color for the little label boxes 30 | container: null, // container (as jQuery object) to put legend in, null means default on top of graph 31 | position: "ne", // position of default legend container within plot 32 | margin: [5, 10], // distance from grid edge to default legend container within plot 33 | backgroundOpacity: 0 // set to 0 to avoid background 34 | }, 35 | grid: { 36 | hoverable: false, 37 | clickable: false 38 | }, 39 | }); 40 | 41 | }); -------------------------------------------------------------------------------- /static/js/excanvas.min.js: -------------------------------------------------------------------------------- 1 | if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&").replace(/"/g,""")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AHAL.x){AL.x=Z.x}if(AG.y==null||Z.yAL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push("')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push("')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push('')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push('','','');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z='';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()}; -------------------------------------------------------------------------------- /static/js/full-calendar/fullcalendar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * FullCalendar v1.5.4 Stylesheet 3 | * 4 | * Copyright (c) 2011 Adam Shaw 5 | * Dual licensed under the MIT and GPL licenses, located in 6 | * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. 7 | * 8 | * Date: Tue Sep 4 23:38:33 2012 -0700 9 | * 10 | */ 11 | #external-events { 12 | float: left; 13 | width: 100%; 14 | padding: 10px; 15 | -moz-box-sizing:border-box; 16 | -webkit-box-sizing:border-box; 17 | box-sizing:border-box; 18 | text-align: left; 19 | } 20 | 21 | 22 | .external-event { /* try to mimick the look of a real event */ 23 | margin: 10px 0; 24 | padding: 3px 9px; 25 | background: #36C; 26 | color: white; 27 | font-size: 1.1em; 28 | line-height:1.8; 29 | cursor: pointer; 30 | } 31 | 32 | #external-events p { 33 | margin: 1.5em 0; 34 | font-size: 11px; 35 | color: #666; 36 | } 37 | 38 | #external-events p input { 39 | margin: 0; 40 | vertical-align: middle; 41 | float:left; 42 | } 43 | #external-events label { 44 | float:left; 45 | line-height: 12px; 46 | padding-left: 7px; 47 | } 48 | 49 | 50 | .fc { 51 | direction: ltr; 52 | text-align: left; 53 | } 54 | 55 | .fc table { 56 | border-collapse: collapse; 57 | border-spacing: 0; 58 | } 59 | 60 | html .fc, 61 | .fc table { 62 | font-size: 1em; 63 | } 64 | 65 | .fc td, 66 | .fc th { 67 | padding: 0; 68 | vertical-align: top; 69 | } 70 | 71 | 72 | 73 | /* Header 74 | ------------------------------------------------------------------------*/ 75 | 76 | .fc-header td { 77 | white-space: nowrap; 78 | } 79 | 80 | .fc-header-left { 81 | width: 25%; 82 | text-align: left; 83 | } 84 | 85 | .fc-header-center { 86 | text-align: center; 87 | } 88 | 89 | .fc-header-right { 90 | width: 25%; 91 | text-align: right; 92 | } 93 | 94 | .fc-header-title { 95 | display: inline-block; 96 | vertical-align: top; 97 | } 98 | 99 | .fc-header-title h2 { 100 | margin-top: 0; 101 | white-space: nowrap; 102 | } 103 | 104 | .fc .fc-header-space { 105 | padding-left: 10px; 106 | } 107 | 108 | .fc-header .fc-button { 109 | margin-bottom: 1em; 110 | vertical-align: top; 111 | } 112 | 113 | /* buttons edges butting together */ 114 | 115 | .fc-header .fc-button { 116 | margin-right: -1px; 117 | } 118 | 119 | .fc-header .fc-corner-right { 120 | margin-right: 1px; /* back to normal */ 121 | } 122 | 123 | .fc-header .ui-corner-right { 124 | margin-right: 0; /* back to normal */ 125 | } 126 | 127 | /* button layering (for border precedence) */ 128 | 129 | .fc-header .fc-state-hover, 130 | .fc-header .ui-state-hover { 131 | z-index: 2; 132 | } 133 | 134 | .fc-header .fc-state-down { 135 | z-index: 3; 136 | } 137 | 138 | .fc-header .fc-state-active, 139 | .fc-header .ui-state-active { 140 | z-index: 4; 141 | } 142 | 143 | 144 | 145 | /* Content 146 | ------------------------------------------------------------------------*/ 147 | 148 | .fc-content { 149 | clear: both; 150 | } 151 | 152 | .fc-view { 153 | width: 100%; /* needed for view switching (when view is absolute) */ 154 | overflow: hidden; 155 | } 156 | 157 | 158 | 159 | /* Cell Styles 160 | ------------------------------------------------------------------------*/ 161 | 162 | .fc-widget-header, /* , usually */ 163 | .fc-widget-content { /* , usually */ 164 | border: 1px solid #ccc; 165 | } 166 | 167 | .fc-state-highlight { /* today cell */ /* TODO: add .fc-today to */ 168 | background: #ffc; 169 | } 170 | 171 | .fc-cell-overlay { /* semi-transparent rectangle while dragging */ 172 | background: #9cf; 173 | opacity: .2; 174 | filter: alpha(opacity=20); /* for IE */ 175 | } 176 | 177 | 178 | 179 | /* Buttons 180 | ------------------------------------------------------------------------*/ 181 | 182 | .fc-button { 183 | position: relative; 184 | display: inline-block; 185 | cursor: pointer; 186 | } 187 | 188 | .fc-state-default { /* non-theme */ 189 | border-style: solid; 190 | border-width: 1px 0; 191 | } 192 | 193 | .fc-button-inner { 194 | position: relative; 195 | float: left; 196 | overflow: hidden; 197 | } 198 | 199 | .fc-state-default .fc-button-inner { /* non-theme */ 200 | border-style: solid; 201 | border-width: 0 1px; 202 | } 203 | 204 | .fc-button-content { 205 | position: relative; 206 | float: left; 207 | height: 1.9em; 208 | line-height: 1.9em; 209 | padding: 0 .6em; 210 | white-space: nowrap; 211 | } 212 | 213 | /* icon (for jquery ui) */ 214 | 215 | .fc-button-content .fc-icon-wrap { 216 | position: relative; 217 | float: left; 218 | top: 50%; 219 | } 220 | 221 | .fc-button-content .ui-icon { 222 | position: relative; 223 | float: left; 224 | margin-top: -50%; 225 | *margin-top: 0; 226 | *top: -50%; 227 | } 228 | 229 | /* gloss effect */ 230 | 231 | .fc-state-default .fc-button-effect { 232 | position: absolute; 233 | top: 50%; 234 | left: 0; 235 | } 236 | 237 | .fc-state-default .fc-button-effect span { 238 | position: absolute; 239 | top: -100px; 240 | left: 0; 241 | width: 500px; 242 | height: 100px; 243 | border-width: 100px 0 0 1px; 244 | border-style: solid; 245 | border-color: #fff; 246 | background: #444; 247 | opacity: .09; 248 | filter: alpha(opacity=9); 249 | } 250 | 251 | /* button states (determines colors) */ 252 | 253 | .fc-state-default, 254 | .fc-state-default .fc-button-inner { 255 | border-style: solid; 256 | border-color: #ccc #bbb #aaa; 257 | background: #F3F3F3; 258 | color: #000; 259 | } 260 | 261 | .fc-state-hover, 262 | .fc-state-hover .fc-button-inner { 263 | border-color: #999; 264 | } 265 | 266 | .fc-state-down, 267 | .fc-state-down .fc-button-inner { 268 | border-color: #555; 269 | background: #777; 270 | } 271 | 272 | .fc-state-active, 273 | .fc-state-active .fc-button-inner { 274 | border-color: #555; 275 | background: #777; 276 | color: #fff; 277 | } 278 | 279 | .fc-state-disabled, 280 | .fc-state-disabled .fc-button-inner { 281 | color: #999; 282 | border-color: #ddd; 283 | } 284 | 285 | .fc-state-disabled { 286 | cursor: default; 287 | } 288 | 289 | .fc-state-disabled .fc-button-effect { 290 | display: none; 291 | } 292 | 293 | 294 | 295 | /* Global Event Styles 296 | ------------------------------------------------------------------------*/ 297 | 298 | .fc-event { 299 | border-style: solid; 300 | border-width: 0; 301 | font-size: .85em; 302 | cursor: default; 303 | } 304 | 305 | a.fc-event, 306 | .fc-event-draggable { 307 | cursor: pointer; 308 | } 309 | 310 | a.fc-event { 311 | text-decoration: none; 312 | } 313 | 314 | .fc-rtl .fc-event { 315 | text-align: right; 316 | } 317 | 318 | .fc-event-skin { 319 | border-color: #36c; /* default BORDER color */ 320 | background-color: #36c; /* default BACKGROUND color */ 321 | color: #fff; /* default TEXT color */ 322 | } 323 | 324 | .fc-event-inner { 325 | position: relative; 326 | width: 100%; 327 | height: 100%; 328 | border-style: solid; 329 | border-width: 0; 330 | overflow: hidden; 331 | } 332 | 333 | .fc-event-time, 334 | .fc-event-title { 335 | padding: 0 1px; 336 | } 337 | 338 | .fc .ui-resizable-handle { /*** TODO: don't use ui-resizable anymore, change class ***/ 339 | display: block; 340 | position: absolute; 341 | z-index: 99999; 342 | overflow: hidden; /* hacky spaces (IE6/7) */ 343 | font-size: 300%; /* */ 344 | line-height: 50%; /* */ 345 | } 346 | 347 | 348 | 349 | /* Horizontal Events 350 | ------------------------------------------------------------------------*/ 351 | 352 | .fc-event-hori { 353 | border-width: 1px 0; 354 | margin-bottom: 1px; 355 | } 356 | 357 | /* resizable */ 358 | 359 | .fc-event-hori .ui-resizable-e { 360 | top: 0 !important; /* importants override pre jquery ui 1.7 styles */ 361 | right: -3px !important; 362 | width: 7px !important; 363 | height: 100% !important; 364 | cursor: e-resize; 365 | } 366 | 367 | .fc-event-hori .ui-resizable-w { 368 | top: 0 !important; 369 | left: -3px !important; 370 | width: 7px !important; 371 | height: 100% !important; 372 | cursor: w-resize; 373 | } 374 | 375 | .fc-event-hori .ui-resizable-handle { 376 | _padding-bottom: 14px; /* IE6 had 0 height */ 377 | } 378 | 379 | 380 | 381 | /* Fake Rounded Corners (for buttons and events) 382 | ------------------------------------------------------------*/ 383 | 384 | .fc-corner-left { 385 | margin-left: 1px; 386 | } 387 | 388 | .fc-corner-left .fc-button-inner, 389 | .fc-corner-left .fc-event-inner { 390 | margin-left: -1px; 391 | } 392 | 393 | .fc-corner-right { 394 | margin-right: 1px; 395 | } 396 | 397 | .fc-corner-right .fc-button-inner, 398 | .fc-corner-right .fc-event-inner { 399 | margin-right: -1px; 400 | } 401 | 402 | .fc-corner-top { 403 | margin-top: 1px; 404 | } 405 | 406 | .fc-corner-top .fc-event-inner { 407 | margin-top: -1px; 408 | } 409 | 410 | .fc-corner-bottom { 411 | margin-bottom: 1px; 412 | } 413 | 414 | .fc-corner-bottom .fc-event-inner { 415 | margin-bottom: -1px; 416 | } 417 | 418 | 419 | 420 | /* Fake Rounded Corners SPECIFICALLY FOR EVENTS 421 | -----------------------------------------------------------------*/ 422 | 423 | .fc-corner-left .fc-event-inner { 424 | border-left-width: 1px; 425 | } 426 | 427 | .fc-corner-right .fc-event-inner { 428 | border-right-width: 1px; 429 | } 430 | 431 | .fc-corner-top .fc-event-inner { 432 | border-top-width: 1px; 433 | } 434 | 435 | .fc-corner-bottom .fc-event-inner { 436 | border-bottom-width: 1px; 437 | } 438 | 439 | 440 | 441 | /* Reusable Separate-border Table 442 | ------------------------------------------------------------*/ 443 | 444 | table.fc-border-separate { 445 | border-collapse: separate; 446 | } 447 | 448 | .fc-border-separate th, 449 | .fc-border-separate td { 450 | border-width: 1px 0 0 1px; 451 | } 452 | 453 | .fc-border-separate th.fc-last, 454 | .fc-border-separate td.fc-last { 455 | border-right-width: 1px; 456 | } 457 | 458 | .fc-border-separate tr.fc-last th, 459 | .fc-border-separate tr.fc-last td { 460 | border-bottom-width: 1px; 461 | } 462 | 463 | .fc-border-separate tbody tr.fc-first td, 464 | .fc-border-separate tbody tr.fc-first th { 465 | border-top-width: 0; 466 | } 467 | 468 | 469 | 470 | /* Month View, Basic Week View, Basic Day View 471 | ------------------------------------------------------------------------*/ 472 | 473 | .fc-grid th { 474 | text-align: center; 475 | } 476 | 477 | .fc-grid .fc-day-number { 478 | float: right; 479 | padding: 0 2px; 480 | } 481 | 482 | .fc-grid .fc-other-month .fc-day-number { 483 | opacity: 0.3; 484 | filter: alpha(opacity=30); /* for IE */ 485 | /* opacity with small font can sometimes look too faded 486 | might want to set the 'color' property instead 487 | making day-numbers bold also fixes the problem */ 488 | } 489 | 490 | .fc-grid .fc-day-content { 491 | clear: both; 492 | padding: 2px 2px 1px; /* distance between events and day edges */ 493 | } 494 | 495 | /* event styles */ 496 | 497 | .fc-grid .fc-event-time { 498 | font-weight: bold; 499 | } 500 | 501 | /* right-to-left */ 502 | 503 | .fc-rtl .fc-grid .fc-day-number { 504 | float: left; 505 | } 506 | 507 | .fc-rtl .fc-grid .fc-event-time { 508 | float: right; 509 | } 510 | 511 | 512 | 513 | /* Agenda Week View, Agenda Day View 514 | ------------------------------------------------------------------------*/ 515 | 516 | .fc-agenda table { 517 | border-collapse: separate; 518 | } 519 | 520 | .fc-agenda-days th { 521 | text-align: center; 522 | } 523 | 524 | .fc-agenda .fc-agenda-axis { 525 | width: 50px; 526 | padding: 0 4px; 527 | vertical-align: middle; 528 | text-align: right; 529 | white-space: nowrap; 530 | font-weight: normal; 531 | } 532 | 533 | .fc-agenda .fc-day-content { 534 | padding: 2px 2px 1px; 535 | } 536 | 537 | /* make axis border take precedence */ 538 | 539 | .fc-agenda-days .fc-agenda-axis { 540 | border-right-width: 1px; 541 | } 542 | 543 | .fc-agenda-days .fc-col0 { 544 | border-left-width: 0; 545 | } 546 | 547 | /* all-day area */ 548 | 549 | .fc-agenda-allday th { 550 | border-width: 0 1px; 551 | } 552 | 553 | .fc-agenda-allday .fc-day-content { 554 | min-height: 34px; /* TODO: doesnt work well in quirksmode */ 555 | _height: 34px; 556 | } 557 | 558 | /* divider (between all-day and slots) */ 559 | 560 | .fc-agenda-divider-inner { 561 | height: 2px; 562 | overflow: hidden; 563 | } 564 | 565 | .fc-widget-header .fc-agenda-divider-inner { 566 | background: #eee; 567 | } 568 | 569 | /* slot rows */ 570 | 571 | .fc-agenda-slots th { 572 | border-width: 1px 1px 0; 573 | } 574 | 575 | .fc-agenda-slots td { 576 | border-width: 1px 0 0; 577 | background: none; 578 | } 579 | 580 | .fc-agenda-slots td div { 581 | height: 20px; 582 | } 583 | 584 | .fc-agenda-slots tr.fc-slot0 th, 585 | .fc-agenda-slots tr.fc-slot0 td { 586 | border-top-width: 0; 587 | } 588 | 589 | .fc-agenda-slots tr.fc-minor th, 590 | .fc-agenda-slots tr.fc-minor td { 591 | border-top-style: dotted; 592 | } 593 | 594 | .fc-agenda-slots tr.fc-minor th.ui-widget-header { 595 | *border-top-style: solid; /* doesn't work with background in IE6/7 */ 596 | } 597 | 598 | 599 | 600 | /* Vertical Events 601 | ------------------------------------------------------------------------*/ 602 | 603 | .fc-event-vert { 604 | border-width: 0 1px; 605 | } 606 | 607 | .fc-event-vert .fc-event-head, 608 | .fc-event-vert .fc-event-content { 609 | position: relative; 610 | z-index: 2; 611 | width: 100%; 612 | overflow: hidden; 613 | } 614 | 615 | .fc-event-vert .fc-event-time { 616 | white-space: nowrap; 617 | font-size: 10px; 618 | } 619 | 620 | .fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ 621 | position: absolute; 622 | z-index: 1; 623 | top: 0; 624 | left: 0; 625 | width: 100%; 626 | height: 100%; 627 | background: #fff; 628 | opacity: .3; 629 | filter: alpha(opacity=30); 630 | } 631 | 632 | .fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */ 633 | .fc-select-helper .fc-event-bg { 634 | display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */ 635 | } 636 | 637 | /* resizable */ 638 | 639 | .fc-event-vert .ui-resizable-s { 640 | bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ 641 | width: 100% !important; 642 | height: 8px !important; 643 | overflow: hidden !important; 644 | line-height: 8px !important; 645 | font-size: 11px !important; 646 | font-family: monospace; 647 | text-align: center; 648 | cursor: s-resize; 649 | } 650 | 651 | .fc-agenda .ui-resizable-resizing { /* TODO: better selector */ 652 | _overflow: hidden; 653 | } 654 | 655 | 656 | -------------------------------------------------------------------------------- /static/js/guidely/guidely-number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lisijie/webcron/fb01a7d021806101d7b5d81592f0bbaabcf7d0aa/static/js/guidely/guidely-number.png -------------------------------------------------------------------------------- /static/js/guidely/guidely.css: -------------------------------------------------------------------------------- 1 | .guidely-number { 2 | background: url(guidely-number.png) no-repeat 0 0; 3 | width: 45px; 4 | height: 45px; 5 | display: none; 6 | position: absolute; 7 | cursor: pointer; 8 | z-index: 10002; 9 | } 10 | 11 | .guidely-number span { 12 | width: 43px; 13 | height: 43px; 14 | font-family: arial, sans-serif; 15 | font-size: 20px; 16 | font-weight: bold; 17 | text-align: center; 18 | color: #FFF; 19 | text-align: center; 20 | display: block; 21 | line-height: 44px; 22 | } 23 | 24 | .guidely-guide { 25 | background: #FFF; 26 | width: 300px; 27 | display: none; 28 | border: 3px solid #999; 29 | 30 | -webkit-border-radius:5px; 31 | -moz-border-radius:5px; 32 | border-radius:5px; 33 | 34 | -webkit-box-shadow:0 0 12px rgba(0,0,0,0.4); 35 | -moz-box-shadow:0 0 12px rgba(0,0,0,0.4); 36 | box-shadow:0 0 12px rgba(0,0,0,0.4); 37 | 38 | z-index: 10001; 39 | } 40 | 41 | .guidely-guide h4 { 42 | font-family: Helvetica, arial, sans-serif; 43 | font-size: 15px; 44 | font-weight: bold; 45 | color: #333; 46 | padding-bottom: 15px !important; 47 | padding: 0; 48 | margin: 0 0 1em; 49 | border-bottom: 1px dotted #CCC; 50 | } 51 | 52 | .guidely-guide-pad { 53 | font-size: 12px; 54 | line-height: 1.7em; 55 | padding: 15px 15px 5px 30px; 56 | 57 | } 58 | 59 | .guidely-anchor-right .guidely-guide-pad { padding: 15px 30px 5px 15px; } 60 | 61 | .guidely-anchor-right .guidely-close-trigger { right: 30px; } 62 | 63 | .guidely-popup 64 | { 65 | color: #444; 66 | display:block; 67 | padding: 0; 68 | background: #fff; 69 | 70 | -webkit-border-top-left-radius: 4px; 71 | -webkit-border-top-right-radius: 4px; 72 | -moz-border-radius-topleft: 4px; 73 | -moz-border-radius-topright: 4px; 74 | border-top-left-radius: 4px; 75 | border-top-right-radius: 4px; 76 | } 77 | 78 | 79 | 80 | 81 | 82 | .guidely-controls { 83 | background: #EEE; 84 | text-align: right; 85 | padding: 7px 10px; 86 | margin-top: 1em; 87 | } 88 | 89 | .guidely-controls button { 90 | font-size: 11px; 91 | padding: 3px 8px; 92 | *padding: 1px 4px; 93 | cursor: pointer; 94 | } 95 | 96 | .guidely-overlay 97 | { 98 | position: fixed; 99 | top: 0px; 100 | left: 0px; 101 | height:100%; 102 | width:100%; 103 | background-color: #000; 104 | z-index: 10000; 105 | 106 | filter: alpha(opacity=30); 107 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=30); 108 | -moz-opacity: 0.30; 109 | opacity:0.30; 110 | } 111 | 112 | .guidely-start-trigger { 113 | background: #444; 114 | background: rgba(0,0,0,.6); 115 | text-decoration: none; 116 | color: #FFF; 117 | cursor: pointer; 118 | padding: 4px 10px 4px 12px; 119 | position: fixed; 120 | top: 0; 121 | right: 0; 122 | 123 | z-index: 9999; 124 | 125 | -webkit-border-bottom-left-radius: 5px; 126 | -moz-border-radius-bottomleft: 5px; 127 | border-bottom-left-radius: 5px; 128 | 129 | } 130 | 131 | .guidely-close-trigger { 132 | font-family: Helvetica, arial, sans-serif; 133 | font-size: 13px; 134 | font-weight: bold; 135 | text-decoration: none; 136 | color: #AAA; 137 | position: absolute; 138 | right:16px; 139 | top: 12px; 140 | } 141 | 142 | #guide-welcome { width: 350px; } 143 | 144 | #guide-welcome .guidely-guide-pad { padding: 15px 15px 5px 15px; } -------------------------------------------------------------------------------- /static/js/guidely/guidely.min.js: -------------------------------------------------------------------------------- 1 | var guidely=(function(){return{_guides:[],_defaults:{showOnStart:true,welcome:true,welcomeTitle:'Welcome to the guided tour!',welcomeText:'Click to start a brief tour of our site. Here we\'ll point out important features and tips to make your experience easier. ',overlay:true,startTrigger:true,escClose:true,keyNav:true,debug:false},_options:{},init:function(config){var that,options;that=this;that._options=$.extend(that._defaults,config);if(that._guides.length<1){that._log('No guides available.');return false;} 2 | that._append();that._createTopAnchor();that.close();if(that._options.startTrigger){that._createStartTrigger();} 3 | if(that._options.showOnStart){if(that._options.welcome){that.welcome();}else{that.start();}}},start:function(){var that=this;that.close();that._scrollToTop();that.showNumbers();that.show(1);that.showOverlay();that._log(that._guides);that._reposition();$(document).bind('keyup.guidely',function(e){if(that._options.escClose){if(e.keyCode==27){that.close();}} 4 | if(that._options.keyNav){if(e.keyCode==37){that.prev();} 5 | if(e.keyCode==39){that.next();}}});$(window).bind('resize.guidely',function(){that._reposition();});},welcome:function(){var that,content,popup,pad,controls,close,overlay,startBtn,noBtn;that=this;that.close();content=$('
',{'id':'guide-welcome','class':'guidely-guide'}).appendTo('body');popup=$('
',{'class':'guidely-popup'}).appendTo(content);pad=$('
',{'class':'guidely-guide-pad'}).appendTo(popup);pad.append('

'+that._options.welcomeTitle+'

');pad.append(that._options.welcomeText);controls=$('
',{'class':'guidely-controls'}).appendTo(popup);content.css({'position':'absolute','top':'75px','left':'50%','margin-left':'-'+content.outerWidth()/2+'px'});close=$('',{'href':'javascript:;','class':'guidely-close-trigger','html':'x','click':function(){that.close();}}).appendTo(content);this.showOverlay();startBtn=$(' 36 | 返 回 37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /views/group/edit.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

编辑分组

8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | 返 回 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 | -------------------------------------------------------------------------------- /views/group/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

分组列表

8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | 批量操作 18 | 19 | 22 |
23 | 24 |
25 | 新建分组 26 |
27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{range $k, $v := .list}} 43 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | {{end}} 56 | 57 | 58 | 59 | 60 |
35 | ID分组名称描述操作
{{$v.Id}}{{$v.GroupName}}{{$v.Description}} 49 | 50 | 编辑 51 | 52 | 53 |
{{str2html .pageBar}}
61 |
62 | 63 |
64 |
65 |
66 |
67 | 68 | -------------------------------------------------------------------------------- /views/help/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

使用帮助

8 |
9 | 10 |
11 |

Cron表达式说明

12 |
13 |

14 | Cron表达式是一个字符串,字符串以空格隔开,分为5或6个域,每一个域代表一个含义,系统支持的表达式格式如下:
15 |

Seconds Minutes Hours DayofMonth Month [DayofWeek]
16 | 其中 DayofWeek 为可选域。 17 |

18 |

19 | 每一个域可出现的字符如下: 20 |

    21 |
  • Seconds: 可出现"* / , -"四个字符,有效范围为0-59的整数
  • 22 |
  • Minutes: 可出现"* / , -"四个字符,有效范围为0-59的整数
  • 23 |
  • Hours: 可出现"* / , -"四个字符,有效范围为0-23的整数
  • 24 |
  • DayofMonth: 可出现"* / , - ?"五个字符,有效范围为0-31的整数
  • 25 |
  • Month: 可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEC
  • 26 |
  • DayofWeek: 可出现"* / , - ?"五个字符,有效范围为0-6的整数或SUN-SAT两个范围。0表示星期天,1表示星期一, 依次类推
  • 27 |
28 |

29 |

30 | 各个字符的含义如下: 31 |

    32 |
  • * 表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。
  • 33 |
  • ? 字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值,当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”。
  • 34 |
  • - 表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次。
  • 35 |
  • / 表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次。
  • 36 |
  • , 表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
  • 37 |
38 |

39 |

40 | 一些例子: 41 |

42 | 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点触发 
43 | 
44 | 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时触发  
45 | 
46 | 0 0 12 ? * WED 表示每个星期三中午12点触发  
47 | 
48 | 0 0 12 * * ? 每天中午12点触发 
49 | 
50 | 0 15 10 ? * * 每天上午10:15触发 
51 | 
52 | 0 15 10 * * ? 每天上午10:15触发 
53 | 
54 | 0 15 10 * * ? * 每天上午10:15触发 
55 | 
56 | 0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 
57 | 
58 | 0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 
59 | 
60 | 0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 
61 | 
62 | 0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 
63 | 
64 | 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 
65 | 
66 | 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 
67 | 
68 | 0 15 10 15 * ? 每月15日上午10:15触发 
69 | 
70 | 0 15 10 L * ? 每月最后一日的上午10:15触发
71 |

72 |
73 |
74 |
75 |
-------------------------------------------------------------------------------- /views/layout/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{.pageTitle}} - {{.siteName}} v{{.version}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 55 | 56 | 88 | 89 |
90 |
91 |
92 | 93 | {{.LayoutContent}} 94 | 95 | 96 |
97 |
98 |
99 | 100 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /views/main/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

即将执行的任务

7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{range $k, $v := .jobs}} 19 | 20 | 21 | 22 | 23 | {{end}} 24 | 25 |
任务名下次执行时间
{{$v.task_name}} # {{$v.task_id}}{{$v.next_time}}
26 |
27 | 28 |
29 | 30 | 31 |
32 |
33 |

执行失败的任务

34 |
35 | 36 |
37 |
    38 | {{range $k, $v := .errLogs}} 39 |
  • 40 | 41 |
    42 | {{$v.task_name}} # {{$v.id}} 43 |

    44 | 开始时间:{{$v.start_time}} | 执行时间:{{$v.process_time}}秒 | 输出:{{$v.ouput_size}} | 状态:{{if eq $v.status 0}} 45 | 正常 46 | {{else if eq $v.status -1}} 47 | 异常 48 | {{else}} 49 | 超时 50 | {{end}} 51 |

    52 |
    {{$v.error}}
    53 |
    54 | 55 |
  • 56 | {{end}} 57 |
58 |
59 | 60 |
61 | 62 | 63 |
64 | 65 |
66 | 67 |
68 |
69 |

最近执行任务

70 |
71 | 72 |
73 |
    74 | {{range $k, $v := .recentLogs}} 75 |
  • 76 | 77 |
    78 | {{$v.task_name}} # {{$v.id}} 79 |

    80 | 开始时间:{{$v.start_time}} | 执行时间:{{$v.process_time}}秒 | 输出:{{$v.ouput_size}} | 状态:{{if eq $v.status 0}} 81 | 正常 82 | {{else if eq $v.status -1}} 83 | 异常 84 | {{else}} 85 | 超时 86 | {{end}} 87 |

    88 |
    {{$v.output}}
    89 |
    90 | 91 |
  • 92 | {{end}} 93 |
94 |
95 | 96 |
97 | 98 | 99 |
100 | 101 |
-------------------------------------------------------------------------------- /views/main/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 登录 - {{.siteName}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /views/main/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |

修改资料

7 |
8 | 9 |
10 | 11 | {{if .flash.error}} 12 | 16 | {{end}} 17 | {{if .flash.success}} 18 |
19 | 20 | {{.flash.success}} 21 |
22 | {{end}} 23 | 24 |
25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 |

不修改密码请留空

46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 | 56 |
57 | 58 | 59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 |
-------------------------------------------------------------------------------- /views/task/add.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

添加任务

8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 | 40 |
41 |
42 | 43 |
44 | 45 |
46 | 49 | 52 |

设为“是”的话,如果该任务在上一个时间点还没执行完,则略过不执行

53 |
54 |
55 | 56 |
57 | 58 |
59 | 60 | 参见使用帮助 61 |
62 |
63 | 64 | 65 |
66 | 67 |
68 | 69 |
70 |
71 | 72 |
73 | 74 |
75 | 秒 76 | 不设置的话,默认超时时间为1天 77 |
78 |
79 | 80 |
81 | 82 |
83 | 86 | 89 | 92 |
93 |
94 | 95 |
96 | 97 |
98 | 99 |

每行一个email地址,如果不需要抄送给其他人请留空

100 |
101 |
102 | 103 |
104 | 105 | 106 |
107 | 108 | 返 回 109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 | 118 | -------------------------------------------------------------------------------- /views/task/edit.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

编辑任务

8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 |
35 | 41 |
42 |
43 | 44 |
45 | 46 |
47 | 50 | 53 |

设为“是”的话,如果该任务在上一个时间点还没执行完,则略过不执行

54 |
55 |
56 | 57 |
58 | 59 |
60 | 61 | 参见使用帮助 62 |
63 |
64 | 65 |
66 | 67 |
68 | 69 |
70 |
71 | 72 |
73 | 74 |
75 | 秒 76 | 不设置的话,默认超时时间为1天 77 |
78 |
79 | 80 |
81 | 82 |
83 | 86 | 89 | 92 |
93 |
94 | 95 |
96 | 97 |
98 | 99 |

每行一个email地址,如果不需要抄送给其他人请留空

100 |
101 |
102 | 103 |
104 | 105 | 106 |
107 | 108 | 返 回 109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 | 118 | -------------------------------------------------------------------------------- /views/task/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

任务列表

8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | 批量操作 18 | 19 | 25 |
26 | 27 |
28 | 新建任务 29 |
30 | 31 |
32 |
33 | 39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {{range $k, $v := .list}} 59 | 60 | 61 | 62 | 72 | 73 | 74 | 75 | 95 | 96 | {{end}} 97 | 98 | 99 | 100 | 101 |
49 | ID任务名称时间表达式上次执行时间下次执行时间操作
{{$v.id}} 63 | {{if eq $v.running 0}} 64 | 65 | {{else}} 66 | 67 | {{end}} 68 | 69 | {{$v.name}} 70 | 71 | {{$v.cron_spec}}{{$v.prev_time}}{{$v.next_time}} 76 | {{if eq $v.status 0}} 77 | 78 | 激活 79 | 80 | {{else}} 81 | 82 | 暂停 83 | 84 | {{end}} 85 | 86 | 编辑 87 | 88 | 89 | 执行 90 | 91 | 92 | 日志 93 | 94 |
{{str2html .pageBar}}
102 |
103 | 104 |
105 |
106 |
107 |
108 | 109 | -------------------------------------------------------------------------------- /views/task/logs.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

任务执行日志

8 |
9 | 10 |
11 | 12 | 13 | 14 |

15 |

{{.task.TaskName}} (#{{.task.Id}})

16 | 17 |
{{.task.Command}}
18 |

19 |
20 | 21 |
22 | 23 |
24 | 批量操作 25 | 26 | 29 |
30 | 31 |
32 | 返 回 33 |
34 | 35 |
36 | 37 |
38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {{range $k, $v := .list}} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 67 | 72 | 73 | {{end}} 74 | 75 | 76 | 77 | 78 |
42 | ID开始时间执行时间输出状态操作
{{$v.id}}{{$v.start_time}}{{$v.process_time}} 秒{{$v.ouput_size}} 59 | {{if eq $v.status 0}} 60 | 正常 61 | {{else if eq $v.status -1}} 62 | 出错 63 | {{else if eq $v.status -2}} 64 | 超时 65 | {{end}} 66 | 68 | 69 | 详情 70 | 71 |
{{str2html .pageBar}}
79 |
80 | 81 |
82 |
83 |
84 |
85 | 86 | -------------------------------------------------------------------------------- /views/task/viewlog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

查看日志详情

8 |
9 | 10 |
11 | 12 |

{{.task.TaskName}}

13 | 日志ID:#{{.data.id}}
14 | 执行时间:{{.data.start_time}}
15 | 执行耗时:{{.data.process_time}}秒
16 | 输出大小:{{.data.ouput_size}} 17 | 18 |
19 |

任务命令

20 |
{{.task.Command}}
21 |
22 | 23 |

执行输出

24 |
{{.data.output}}
25 | 26 |

错误输出

27 |
{{.data.error}}
28 |
29 | 30 |
31 | 返 回 32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | --------------------------------------------------------------------------------