├── .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 | 
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=$(' |