├── static ├── img │ ├── build.png │ ├── guy.jpg │ ├── refresh.png │ └── refresh2.png ├── layer │ ├── skin │ │ ├── default │ │ │ ├── textbg.png │ │ │ ├── icon_ext.png │ │ │ ├── xubox_ico0.png │ │ │ ├── xubox_title0.png │ │ │ ├── xubox_loading0.gif │ │ │ ├── xubox_loading1.gif │ │ │ ├── xubox_loading2.gif │ │ │ └── xubox_loading3.gif │ │ ├── layer.ext.css │ │ └── layer.css │ ├── extend │ │ └── layer.ext.js │ └── layer.min.js ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.svg ├── css │ ├── g.css │ └── select2-bootstrap.css └── js │ ├── g.js │ └── bootstrap.min.js ├── tpls ├── dir │ ├── gunicorn │ │ ├── gunicorn.conf │ │ └── control │ ├── nodejs │ │ └── control │ ├── tomcat6 │ │ └── control │ ├── tomcat7 │ │ └── control │ └── php │ │ └── control ├── nodejs.tpl ├── tomcat6.tpl ├── tomcat7.tpl ├── gunicorn.tpl └── php.tpl ├── models ├── dto.go ├── build.go ├── user.go └── models.go ├── .gitignore ├── main.go ├── docker_build.sh ├── g ├── sso.go └── g.go ├── routers ├── router.go ├── api.go ├── page.go ├── base.go └── index.go ├── conf └── app.conf.example ├── views ├── history.html ├── progress.html ├── index.html └── layout.html ├── schema.sql └── README.md /static/img/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/img/build.png -------------------------------------------------------------------------------- /static/img/guy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/img/guy.jpg -------------------------------------------------------------------------------- /static/img/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/img/refresh.png -------------------------------------------------------------------------------- /static/img/refresh2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/img/refresh2.png -------------------------------------------------------------------------------- /static/layer/skin/default/textbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/textbg.png -------------------------------------------------------------------------------- /static/layer/skin/default/icon_ext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/icon_ext.png -------------------------------------------------------------------------------- /static/layer/skin/default/xubox_ico0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/xubox_ico0.png -------------------------------------------------------------------------------- /static/layer/skin/default/xubox_title0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/xubox_title0.png -------------------------------------------------------------------------------- /tpls/dir/gunicorn/gunicorn.conf: -------------------------------------------------------------------------------- 1 | workers = 4 2 | bind = '0.0.0.0:8080' 3 | limit_request_field_size = 0 4 | limit_request_line = 0 5 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/layer/skin/default/xubox_loading0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/xubox_loading0.gif -------------------------------------------------------------------------------- /static/layer/skin/default/xubox_loading1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/xubox_loading1.gif -------------------------------------------------------------------------------- /static/layer/skin/default/xubox_loading2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/xubox_loading2.gif -------------------------------------------------------------------------------- /static/layer/skin/default/xubox_loading3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/layer/skin/default/xubox_loading3.gif -------------------------------------------------------------------------------- /models/dto.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ReturnDto struct { 4 | Msg string `json:"msg"` 5 | Data interface{} `json:"data"` 6 | } 7 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinp/builder/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | *.swo 4 | *.so 5 | /builder 6 | /paas-builder 7 | /builder.tar.gz 8 | .DS_Store 9 | /_tmp 10 | /t 11 | /_log 12 | /conf/app.conf 13 | 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | _ "github.com/dinp/builder/g" 6 | _ "github.com/dinp/builder/routers" 7 | ) 8 | 9 | func main() { 10 | beego.Run() 11 | } 12 | -------------------------------------------------------------------------------- /tpls/nodejs.tpl: -------------------------------------------------------------------------------- 1 | FROM {{.Registry}}/nodejs:base 2 | 3 | WORKDIR {{.AppDir}} 4 | 5 | ADD dir/nodejs/* {{.AppDir}}/ 6 | ADD {{.Tarball}} {{.AppDir}}/ 7 | 8 | EXPOSE 8080 9 | 10 | RUN chmod +x control 11 | CMD ["./control", "start", "8080"] 12 | 13 | -------------------------------------------------------------------------------- /tpls/dir/nodejs/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/logs 4 | touch /opt/logs/std.log 5 | 6 | sed -i -e "s/\$SCRIBE_IP/$SCRIBE_IP/g" /etc/supervisord.d/scribed.conf 7 | sed -i -e "s/\$APP_NAME/$APP_NAME/g" /etc/supervisord.d/scribed.conf 8 | 9 | supervisord -n -c /etc/supervisord.conf 10 | 11 | -------------------------------------------------------------------------------- /tpls/tomcat6.tpl: -------------------------------------------------------------------------------- 1 | FROM {{.Registry}}/javaweb:super 2 | 3 | RUN mkdir -p /opt/bin 4 | ADD dir/tomcat6/* /opt/bin/ 5 | 6 | ADD {{.Tarball}} {{.AppDir}}/ 7 | 8 | WORKDIR {{.AppDir}} 9 | RUN unzip -q {{.Tarball}} && rm -rf {{.Tarball}} 10 | 11 | EXPOSE 8080 12 | 13 | RUN chmod +x /opt/bin/control 14 | CMD ["/opt/bin/control", "start", "8080"] 15 | 16 | -------------------------------------------------------------------------------- /tpls/tomcat7.tpl: -------------------------------------------------------------------------------- 1 | FROM {{.Registry}}/tomcat:7.0.56 2 | 3 | RUN mkdir -p /opt/bin 4 | ADD dir/tomcat7/* /opt/bin/ 5 | 6 | ADD {{.Tarball}} {{.AppDir}}/ 7 | 8 | WORKDIR {{.AppDir}} 9 | RUN unzip -q {{.Tarball}} && rm -rf {{.Tarball}} 10 | 11 | EXPOSE 8080 12 | 13 | RUN chmod +x /opt/bin/control 14 | CMD ["/opt/bin/control", "start", "8080"] 15 | 16 | -------------------------------------------------------------------------------- /tpls/dir/gunicorn/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/logs 4 | 5 | touch /opt/logs/error.log 6 | touch /opt/logs/std.log 7 | 8 | sed -i -e "s/\$SCRIBE_IP/$SCRIBE_IP/g" /etc/supervisord.d/scribed.conf 9 | sed -i -e "s/\$APP_NAME/$APP_NAME/g" /etc/supervisord.d/scribed.conf 10 | 11 | /usr/local/bin/python /usr/local/bin/supervisord -n -c /etc/supervisord.conf 12 | -------------------------------------------------------------------------------- /tpls/dir/tomcat6/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/logs 4 | 5 | touch /opt/tomcat/logs/localhost_access_log.txt 6 | touch /opt/tomcat/logs/catalina.out 7 | 8 | sed -i -e "s/\$SCRIBE_IP/$SCRIBE_IP/g" /etc/supervisord.d/scribed.conf 9 | sed -i -e "s/\$APP_NAME/$APP_NAME/g" /etc/supervisord.d/scribed.conf 10 | 11 | supervisord -n -c /etc/supervisord.conf 12 | -------------------------------------------------------------------------------- /tpls/dir/tomcat7/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/logs 4 | 5 | touch /opt/tomcat/logs/localhost_access_log.txt 6 | touch /opt/tomcat/logs/catalina.out 7 | 8 | sed -i -e "s/\$SCRIBE_IP/$SCRIBE_IP/g" /etc/supervisord.d/scribed.conf 9 | sed -i -e "s/\$APP_NAME/$APP_NAME/g" /etc/supervisord.d/scribed.conf 10 | 11 | supervisord -n -c /etc/supervisord.conf 12 | -------------------------------------------------------------------------------- /tpls/dir/php/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/logs 4 | 5 | touch /opt/php/var/log/php-fpm.log 6 | touch /opt/nginx/logs/access.log 7 | touch /opt/nginx/logs/error.log 8 | 9 | sed -i -e "s/\$SCRIBE_IP/$SCRIBE_IP/g" /etc/supervisord.d/scribed.conf 10 | sed -i -e "s/\$APP_NAME/$APP_NAME/g" /etc/supervisord.d/scribed.conf 11 | 12 | supervisord -n -c /etc/supervisord.conf 13 | -------------------------------------------------------------------------------- /docker_build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | WORKDIR=$1 4 | USERNAME=$2 5 | APPNAME=$3 6 | VERSION=$4 7 | REGISTRY=$5 8 | LOGFILE=$6 9 | 10 | cd $WORKDIR 11 | 12 | cp -r ../../tpls/dir . 13 | 14 | docker build -t $USERNAME/$APPNAME:$VERSION . >> $LOGFILE 15 | docker tag $USERNAME/$APPNAME:$VERSION $REGISTRY/$USERNAME/$APPNAME:$VERSION >> $LOGFILE 16 | docker push $REGISTRY/$USERNAME/$APPNAME:$VERSION >> $LOGFILE 17 | -------------------------------------------------------------------------------- /tpls/gunicorn.tpl: -------------------------------------------------------------------------------- 1 | FROM {{.Registry}}/python:super 2 | 3 | WORKDIR {{.AppDir}} 4 | 5 | ADD dir/gunicorn/* {{.AppDir}}/ 6 | ADD {{.Tarball}} {{.AppDir}}/ 7 | 8 | RUN [[ -f requirements.txt ]] && pip install -r requirements.txt || echo "no requirements.txt" 9 | RUN [[ -f pip_requirements.txt ]] && pip install -r pip_requirements.txt || echo "no pip_requirements.txt" 10 | 11 | EXPOSE 8080 12 | 13 | RUN chmod +x control 14 | CMD ["./control", "start", "8080"] 15 | 16 | -------------------------------------------------------------------------------- /g/sso.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/httplib" 6 | ) 7 | 8 | func GetSig() (sig string, err error) { 9 | uri := fmt.Sprintf("%s/sso/sig", UicInternal) 10 | req := httplib.Get(uri) 11 | sig, err = req.String() 12 | return 13 | } 14 | 15 | func Logout(sig string) error { 16 | uri := fmt.Sprintf("%s/sso/logout/%s", UicInternal, sig) 17 | req := httplib.Get(uri) 18 | req.Param("token", Token) 19 | _, err := req.String() 20 | return err 21 | } 22 | -------------------------------------------------------------------------------- /routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | ) 6 | 7 | func init() { 8 | beego.Router("/", &MainController{}) 9 | beego.Router("/health", &ApiController{}, "get:Health") 10 | beego.Router("/logout", &ApiController{}, "get:Logout") 11 | beego.Router("/build", &MainController{}, "post:Build") 12 | beego.Router("/progress/:id:int", &MainController{}, "get:Progress") 13 | beego.Router("/history", &MainController{}, "get:History;post:DeleteHistory") 14 | beego.Router("/log/:id:int", &MainController{}, "get:Log") 15 | } 16 | -------------------------------------------------------------------------------- /conf/app.conf.example: -------------------------------------------------------------------------------- 1 | appname = builder 2 | httpport = 7788 3 | runmode = prod 4 | 5 | dbuser = builder 6 | dbpass = 7 | dbhost = 127.0.0.1 8 | dbport = 3306 9 | dbname = builder 10 | 11 | tmpdir = ./_tmp 12 | logdir = ./_log 13 | 14 | # in minute 15 | buildtimeout = 15 16 | 17 | uicinternal = http://10.1.2.3:8081 18 | uicexternal = http://10.1.2.3:8081 19 | registry = 10.1.2.3:5000 20 | 21 | buildscript = ./docker_build.sh 22 | tplmapping = tomcat6=>tpls/tomcat6.tpl, tomcat7=>tpls/tomcat7.tpl, php=>tpls/php.tpl, gunicorn=>tpls/gunicorn.tpl, nodejs=>tpls/nodejs.tpl 23 | 24 | token = same-to-uic-token 25 | -------------------------------------------------------------------------------- /routers/api.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/dinp/builder/g" 5 | ) 6 | 7 | type ApiController struct { 8 | BaseController 9 | } 10 | 11 | func (this *ApiController) Health() { 12 | this.Ctx.WriteString("ok") 13 | } 14 | 15 | func (this *ApiController) Logout() { 16 | sig := this.Ctx.GetCookie("sig") 17 | if sig == "" { 18 | this.Ctx.WriteString("u'r not login") 19 | return 20 | } 21 | 22 | err := g.Logout(sig) 23 | if err != nil { 24 | this.Ctx.WriteString("logout from uic fail") 25 | return 26 | } 27 | 28 | this.Ctx.SetCookie("sig", "", 0, "/") 29 | this.Redirect("/", 302) 30 | } 31 | -------------------------------------------------------------------------------- /models/build.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | ) 7 | 8 | func Builds() orm.QuerySeter { 9 | return orm.NewOrm().QueryTable(new(Build)) 10 | } 11 | 12 | func BuildHistory(userId int64) []Build { 13 | var builds []Build 14 | Builds().Filter("UserId", userId).OrderBy("-CreateAt").All(&builds) 15 | return builds 16 | } 17 | 18 | func DeleteHistory(hid, uid int64) error { 19 | exist := Builds().Filter("Id", hid).Filter("UserId", uid).Exist() 20 | if exist { 21 | _, err := Builds().Filter("Id", hid).Delete() 22 | return err 23 | } else { 24 | return fmt.Errorf("no privilege") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tpls/php.tpl: -------------------------------------------------------------------------------- 1 | FROM {{.Registry}}/nginxphp:super 2 | 3 | RUN mkdir -p /opt/t 4 | 5 | ADD {{.Tarball}} /opt/t/ 6 | 7 | RUN [[ -f /opt/t/config/nginx.conf ]] && mv /opt/t/config/nginx.conf /opt/nginx/conf/nginx.conf || echo "no nginx.conf" 8 | RUN [[ -f /opt/t/config/php.ini ]] && mv /opt/t/config/php.ini /opt/php/etc/php.ini || echo "no php.ini" 9 | RUN [[ -f /opt/t/config/php-fpm.conf ]] && mv /opt/t/config/php-fpm.conf /opt/php/etc/php-fpm.conf || echo "no php-fpm.conf" 10 | 11 | RUN rm -rf {{.AppDir}} 12 | RUN mv /opt/t/htdocs {{.AppDir}} 13 | 14 | RUN mkdir -p /opt/bin 15 | ADD dir/php/* /opt/bin/ 16 | 17 | WORKDIR {{.AppDir}} 18 | 19 | EXPOSE 8080 20 | 21 | RUN chmod +x /opt/bin/control 22 | CMD ["/opt/bin/control", "start", "8080"] 23 | 24 | -------------------------------------------------------------------------------- /views/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{range .BuildHistory}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{end}} 23 | 24 |
AppVersionResumeImageCreateAtOperation
{{.App}}{{.Version}}{{.Resume}}{{.Image}}{{dateformat .CreateAt "01-02 15:04"}}
25 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | USE builder; 2 | 3 | SET NAMES utf8; 4 | 5 | DROP TABLE IF EXISTS build; 6 | CREATE TABLE build ( 7 | id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 8 | app VARCHAR(64) NOT NULL DEFAULT '', 9 | version VARCHAR(64) NOT NULL DEFAULT '', 10 | resume VARCHAR(255) NOT NULL DEFAULT '', 11 | base VARCHAR(1024) NOT NULL DEFAULT '', 12 | image VARCHAR(1024) NOT NULL DEFAULT '', 13 | tarball VARCHAR(1024) NOT NULL DEFAULT '', 14 | repo VARCHAR(255) NOT NULL DEFAULT '', 15 | branch VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'branch or tag', 16 | status VARCHAR(255) NOT NULL DEFAULT 'saved in db', 17 | user_id INT UNSIGNED NOT NULL, 18 | user_name VARCHAR(64) NOT NULL, 19 | create_at DATETIME NOT NULL, 20 | KEY idx_build_app(app), 21 | KEY idx_build_user_id(user_id) 22 | ) 23 | ENGINE =innodb 24 | DEFAULT CHARSET =utf8 25 | COLLATE =utf8_general_ci; 26 | 27 | 28 | -------------------------------------------------------------------------------- /static/css/g.css: -------------------------------------------------------------------------------- 1 | body, input, div, span, h1, h2, h3, h4, h5, table, th, td, ul, li, dl, 2 | dt, dd, a { 3 | font-family: 'verdana', 'Microsoft YaHei', 'Consolas', 4 | 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono'; 5 | } 6 | 7 | .cut-line { 8 | margin: 0 5px; 9 | color: #D6D6D6; 10 | } 11 | 12 | .red { 13 | color: red; 14 | } 15 | 16 | .orange { 17 | color: orange; 18 | } 19 | 20 | .green { 21 | color: green; 22 | } 23 | 24 | .blue { 25 | color: blue; 26 | } 27 | 28 | .gray { 29 | color: #999 !important; 30 | } 31 | 32 | .mt0 { 33 | margin-top: 0px; 34 | } 35 | 36 | .mt10 { 37 | margin-top: 10px !important; 38 | } 39 | 40 | .mt20 { 41 | margin-top: 20px !important; 42 | } 43 | 44 | .thin-border { 45 | padding: 10px; 46 | line-height: 1; 47 | border: 1px solid #ddd; 48 | -webkit-border-radius: 4px; 49 | -moz-border-radius: 4px; 50 | border-radius: 4px; 51 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); 52 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); 53 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); 54 | background-color: #fff; 55 | } 56 | -------------------------------------------------------------------------------- /routers/page.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dinp/builder/g" 6 | "github.com/dinp/builder/models" 7 | ) 8 | 9 | type PageController struct { 10 | BaseController 11 | } 12 | 13 | func (this *PageController) hasBusinessCookie() bool { 14 | if this.CurrentUser != nil { 15 | return true 16 | } 17 | 18 | sigInCookie := this.Ctx.GetCookie("sig") 19 | if sigInCookie != "" { 20 | u, err := models.GetUser(sigInCookie) 21 | if err != nil || u == nil { 22 | return false 23 | } 24 | 25 | this.CurrentUser = u 26 | return true 27 | } 28 | 29 | return false 30 | } 31 | 32 | func (this *PageController) CheckLogin() { 33 | if this.hasBusinessCookie() { 34 | this.Data["CurrentUser"] = this.CurrentUser 35 | return 36 | } 37 | 38 | sig, err := g.GetSig() 39 | if err != nil { 40 | this.ServeErrJson(fmt.Sprintf("curl get sig fail: %v", err)) 41 | return 42 | } 43 | 44 | this.Ctx.SetCookie("sig", sig, 2592000, "/") 45 | this.Redirect(fmt.Sprintf("%s/auth/login?sig=%s&callback=http://%s:%d%s", g.UicExternal, sig, this.Ctx.Input.Host(), this.Ctx.Input.Port(), this.Ctx.Input.Uri()), 302) 46 | } 47 | -------------------------------------------------------------------------------- /static/js/g.js: -------------------------------------------------------------------------------- 1 | function err_message_quietly(msg, f) { 2 | $.layer({ 3 | title : false, 4 | closeBtn : false, 5 | time : 2, 6 | dialog : { 7 | msg : msg 8 | }, 9 | end : f 10 | }); 11 | } 12 | 13 | function ok_message_quietly(msg, f) { 14 | $.layer({ 15 | title : false, 16 | closeBtn : false, 17 | time : 1, 18 | dialog : { 19 | msg : msg, 20 | type : 1 21 | }, 22 | end : f 23 | }); 24 | } 25 | 26 | function my_confirm(msg, btns, yes_func, no_func) { 27 | $.layer({ 28 | shade : [ 0 ], 29 | area : [ 'auto', 'auto' ], 30 | dialog : { 31 | msg : msg, 32 | btns : 2, 33 | type : 4, 34 | btn : btns, 35 | yes : yes_func, 36 | no : no_func 37 | } 38 | }); 39 | } 40 | 41 | function handle_quietly(json, f) { 42 | if (json.msg.length > 0) { 43 | err_message_quietly(json.msg); 44 | } else { 45 | ok_message_quietly("successfully:-)", f); 46 | } 47 | } 48 | 49 | // - business function - 50 | function delete_history(hid) { 51 | $.post("/history", {"id": hid}, function(json) { 52 | handle_quietly(json, function() { 53 | location.reload(); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /views/progress.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 |

10 |
11 |
12 |
13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/astaxie/beego/httplib" 7 | "github.com/dinp/builder/g" 8 | ) 9 | 10 | type User struct { 11 | Id int64 `json:"id"` 12 | Name string `json:"name"` 13 | Email string `json:"email"` 14 | Im string `json:"im"` 15 | Phone string `json:"phone"` 16 | } 17 | 18 | func GetUser(sig string) (*User, error) { 19 | key := fmt.Sprintf("u:%s", sig) 20 | u := g.Cache.Get(key) 21 | if u != nil { 22 | uobj := u.(User) 23 | return &uobj, nil 24 | } 25 | 26 | uri := fmt.Sprintf("%s/sso/user/%s", g.UicInternal, sig) 27 | req := httplib.Get(uri) 28 | req.Param("token", g.Token) 29 | resp, err := req.Response() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if resp.StatusCode != 200 { 35 | return nil, fmt.Errorf("StatusCode: %d", resp.StatusCode) 36 | } 37 | 38 | decoder := json.NewDecoder(resp.Body) 39 | type TmpStruct struct { 40 | User *User `json:"user"` 41 | } 42 | var t TmpStruct 43 | err = decoder.Decode(&t) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | // don't worry cache expired. we just use username which can not modify 49 | g.Cache.Put(key, *t.User, int64(360000)) 50 | 51 | return t.User, nil 52 | } 53 | -------------------------------------------------------------------------------- /routers/base.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/astaxie/beego" 5 | "github.com/dinp/builder/models" 6 | "github.com/toolkits/web" 7 | "strconv" 8 | ) 9 | 10 | type Checker interface { 11 | CheckLogin() 12 | } 13 | 14 | type BaseController struct { 15 | beego.Controller 16 | CurrentUser *models.User 17 | } 18 | 19 | func (this *BaseController) Prepare() { 20 | if app, ok := this.AppController.(Checker); ok { 21 | app.CheckLogin() 22 | } 23 | } 24 | 25 | func (this *BaseController) SetPaginator(per int, nums int64) *web.Paginator { 26 | p := web.NewPaginator(this.Ctx.Request, per, nums) 27 | this.Data["paginator"] = p 28 | return p 29 | } 30 | 31 | func (this *BaseController) GetIntWithDefault(paramKey string, defaultVal int) int { 32 | valStr := this.GetString(paramKey) 33 | var val int 34 | if valStr == "" { 35 | val = defaultVal 36 | } else { 37 | var err error 38 | val, err = strconv.Atoi(valStr) 39 | if err != nil { 40 | val = defaultVal 41 | } 42 | } 43 | return val 44 | } 45 | 46 | func (this *BaseController) ServeErrJson(msg string) { 47 | this.Data["json"] = &models.ReturnDto{Msg: msg} 48 | this.ServeJson() 49 | } 50 | 51 | func (this *BaseController) ServeOKJson() { 52 | this.Data["json"] = &models.ReturnDto{Msg: ""} 53 | this.ServeJson() 54 | } 55 | 56 | func (this *BaseController) ServeDataJson(val interface{}) { 57 | this.Data["json"] = &models.ReturnDto{Msg: "", Data: val} 58 | this.ServeJson() 59 | } 60 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

填写以下信息build docker image

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

PaaS Builder

36 |
37 |
38 | welcome, {{.CurrentUser.Name}} ¦ logout 40 |
41 |
42 |
43 |
44 | History 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{.LayoutContent}} 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Builder 2 | ======= 3 | 4 | 这是DINP中的一个编配平台,核心作用就是把用户提供的代码包,变成一个image推送到Docker-Registry 5 | 6 | # 设计理念 7 | 8 | - 平台不管编译,用户自己编译好,然后把编译好的代码扔给平台 9 | - 不管是什么语言的代码,最终都打包成一个docker image,通过docker来做规范化 10 | 11 | ## 编译与否的问题 12 | 13 | 用户的代码要部署在PaaS平台上,首先要做的就是把代码扔给平台,那么问题来了,通过什么方式扔给平台呢?通过git?还是直接上传?扔上来的是源代码?还是已经编译完成的?解释型语言当然是不需要编译的,但是编译型的呢,比如golang,java,我们要让用户给我们.go、.java文件然后我们去编译?还是用户编译好,直接给我们二进制,给我们.class文件? 14 | 15 | 平台刚起步,我们希望在可接受的范围内,做的事情越少越好,这样出错的几率就越少,之后平台慢慢发展,可以再加入一些新的feature。所以我们选择让用户把编译之后的代码交给我们,比如java的话,就扔个war包给我们即可;golang的话需要编译成二进制(编译成64位Linux的,因为平台是64位Linux)。 16 | 17 | 有些PaaS,比如tsuru,是让用户把代码push到某个指定repo的指定分支的,然后后端的git receiver就会触发编译、上线脚本。看起来是挺好的,但是这样处理比较麻烦,坑太多,以后再说吧 18 | 19 | 接下来是要确定通过什么方式把代码扔给平台,我们目前支持两种方式,一个是在页面直接上传,一个是给一个可以下载的http地址,平台去下载。 20 | 21 | ## 使用docker来做规范化打包 22 | 23 | 用户的代码可能是不同语言写的,比较常见的比如Java、PHP、Python,用户给我们的是一个tar.gz包,我们要怎么把其变成一个docker image呢? 24 | 25 | 最简单的实现方式:dinp提供一个base image,里边就只是Ubuntu或者centos的环境,用户自己搞定runtime依赖,比如Java的话,用户的tar.gz包中应该包含JDK、tomcat、webapp,然后约定一个启动脚本,比如就是根目录的control文件,平台只需要`./control start`即可启动。如果用户是PHP的代码,需要把nginx、php-fpm、code打包进来。 26 | 27 | 但是这种方式太麻烦,如果是golang的话可以使用上面的方式,因为golang是静态编译的,也没啥依赖。Java、php、Python、Ruby让用户这么搞,那不得哭了…… 28 | 29 | 我们可以针对不同的语言做不同的base image,比如Java的,base image中提前把JDK和tomcat做好,用户直接上传一个war包,搞定!比如PHP的base image,提前把nginx、php-fpm之类的做好,用户直接把php code上传,搞定! 30 | 31 | 嗯,这也是Builder平台现在采用的方式,我们做了一些base image,让用户在页面上选择使用哪个,用户选择了base image,也就间接的告诉了我们他是什么类型的代码了(比如java的程序肯定要选择一个java的base image)。然后我们根据不同类型的程序生成一个Dockerfile,把用户的代码和base image揉在一起,搞定! 32 | 33 | # 安装方法 34 | 35 | 代码在 [这里](https://github.com/dinp/builder) ,这是个golang的项目,使用beego框架,安装起来也比较简单,就是通常的golang项目的编译方式 36 | 37 | mkdir -p $GOPATH/src/github.com/dinp # 假设你已经配置好了GOROOT和GOPATH 38 | cd $GOPATH/src/github.com/dinp 39 | git clone https://github.com/dinp/builder.git 40 | cd builder 41 | go get ./... 42 | go build 43 | mv conf/app.conf.example conf/app.conf 44 | # modify conf/app.conf 45 | ./builder 46 | 47 | 项目采用[UIC](http://ulricqin.com/project/uic/)作为单点登录系统,所以要想运行测试的话得先把UIC搭建起来 48 | 49 | 简单解释一下`conf/app.conf`各项配置的作用 50 | 51 | - appname、httpport没啥好说的 52 | - runmode取值是dev或者prod,可以参看beego的文档 53 | - db打头的是数据库配置,数据库初始化脚本在schema.sql 54 | - tmpdir、logdir是就是一些临时目录,不解释 55 | - buildtimeout是编译超时时间,超过了这个时间就会被kill,单位是分钟 56 | - uicinternal、uicexternal是UIC的配置,为啥分成两个呢?Builder和UIC通常是在一个内网的,相互之间的访问可以走内网,所以有个uicinternal,但是有的时候UIC的内网地址用户是没法访问的,所以在sso登录的时候还是需要redirect到UIC的外网地址 57 | - registry是docker私有源,读者可以使用docker registry搭建 58 | - buildscript是Builder内部用到的一个脚本文件地址,默认配置即可 59 | - tplmapping这个很重要,这是base image的配置,在registry中增加了base image,也要在此配置一下,这样用户才能在页面上看到 60 | - token,这是与UIC通信的凭证,与UIC的token配置成一样即可 61 | 62 | 63 | # 问题 64 | 65 | 编配的时候用户要查看log,这造成了单点状态,目前可以通过挂载分布式文件系统解决 66 | -------------------------------------------------------------------------------- /static/layer/skin/layer.ext.css: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @Name: layer拓展样式 4 | @Date: 2012.12.13 5 | @Author: 贤心 6 | @blog: sentsin.com 7 | 8 | **/ 9 | 10 | .xubox_iconext{background:url(default/icon_ext.png) no-repeat;} 11 | 12 | /* prompt模式 */ 13 | .xubox_layer .xubox_form{width:240px; height:30px; line-height:30px; padding: 0 5px; border: 1px solid #ccc; background: url(default/textbg.png) #fff repeat-x; color:#333;} 14 | .xubox_layer .xubox_formArea{width:300px; height:100px; line-height:20px;} 15 | 16 | /* tab模式 */ 17 | .xubox_layer .xubox_tab{position:relative; background-color:#fff; box-shadow:1px 1px 50px rgba(0,0,0,.4)} 18 | .xubox_layer .xubox_tabmove{position:absolute; width:600px; height:30px; top:0; left:0;} 19 | .xubox_layer .xubox_tabtit{ display:block; height:34px; border-bottom:1px solid #ccc; background-color:#eee;} 20 | .xubox_layer .xubox_tabtit span{position:relative; float:left; width:120px; height:34px; line-height:34px; text-align:center; cursor:default;} 21 | .xubox_layer .xubox_tabtit span.xubox_tabnow{left:-1px; _top:1px; height:35px; border-left:1px solid #ccc; border-right:1px solid #ccc; background-color:#fff; z-index:10;} 22 | .xubox_layer .xubox_tab_main{line-height:24px; clear:both;} 23 | .xubox_layer .xubox_tab_main .xubox_tabli{display:none;} 24 | .xubox_layer .xubox_tab_main .xubox_tabli.xubox_tab_layer{display:block;} 25 | .xubox_layer .xubox_tabclose{position:absolute; right:10px; top:5px; cursor:pointer;} 26 | 27 | /* photo模式 */ 28 | .xubox_bigimg, .xubox_intro{height:300px} 29 | .xubox_bigimg{position:relative; display:block; width:600px; text-align:center; background:url(default/xubox_loading1.gif) center center no-repeat #000; overflow:hidden; } 30 | .xubox_bigimg img{position:relative; display:inline-block; visibility: hidden;} 31 | .xubox_intro{position:absolute; right:-315px; top:0; width:300px; background-color:#fff; overflow-x:hidden; overflow-y:auto;} 32 | .xubox_imgsee{display:none;} 33 | .xubox_prev, .xubox_next{position:absolute; top:50%; width:27px; _width:44px; height:44px; margin-top:-22px; outline:none;blr:expression(this.onFocus=this.blur());} 34 | .xubox_prev{left:10px; background-position:-5px -5px; _background-position:-70px -5px;} 35 | .xubox_prev:hover{background-position:-33px -5px; _background-position:-120px -5px;} 36 | .xubox_next{right:10px; _right:8px; background-position:-5px -50px; _background-position:-70px -50px;} 37 | .xubox_next:hover{background-position:-33px -50px; _background-position:-120px -50px;} 38 | .xubox_imgbar{position:absolute; left:0; bottom:0; width:100%; height:32px; line-height:32px; background-color:rgba(0,0,0,.8); background-color:#000\9; filter:Alpha(opacity=80); color:#fff; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size:0;} 39 | .xubox_imgtit{/*position:absolute; left:20px;*/} 40 | .xubox_imgtit *{display:inline-block; *display:inline; *zoom:1; vertical-align:top; font-size:12px;} 41 | .xubox_imgtit a{max-width:65%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; color:#fff;} 42 | .xubox_imgtit a:hover{color:#fff; text-decoration:underline;} 43 | .xubox_imgtit em{padding-left:10px;} 44 | 45 | 46 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "github.com/dinp/builder/g" 7 | _ "github.com/go-sql-driver/mysql" 8 | systool "github.com/toolkits/sys" 9 | "html/template" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "time" 14 | ) 15 | 16 | type Build struct { 17 | Id int64 `json:"id"` 18 | App string `json:"app" orm:"size(64)"` 19 | Version string `json:"version" orm:"size(64)"` 20 | Resume string `json:"resume" orm:"size(255)"` 21 | Base string `json:"base" orm:"size(1024)"` 22 | Image string `json:"image" orm:"size(1024)"` 23 | Tarball string `json:"tarball" orm:"size(1024)"` 24 | Repo string `json:"repo" orm:"size(255)"` 25 | Branch string `json:"branch" orm:"size(64)"` 26 | Status string `json:"status" orm:"size(255)"` 27 | UserId int64 `json:"user_id" orm:"index"` 28 | UserName string `json:"user_name" orm:"size(64)"` 29 | CreateAt time.Time `json:"create_at" orm:"auto_now_add;type(datetime)"` 30 | } 31 | 32 | func (this *Build) TableEngine() string { 33 | return "INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci" 34 | } 35 | 36 | func init() { 37 | orm.RegisterModel(new(Build)) 38 | } 39 | 40 | func (this *Build) UpdateBuildStatus(status string) (int64, error) { 41 | this.Status = status 42 | return orm.NewOrm().Update(this, "Status") 43 | } 44 | 45 | func (this *Build) UpdateImage(image string) (int64, error) { 46 | this.Image = image 47 | return orm.NewOrm().Update(this, "Image") 48 | } 49 | 50 | func (this *Build) GenDockerfile(workDir, tarball string) error { 51 | tpl, ok := g.TplMapping[this.Base] 52 | if !ok { 53 | return fmt.Errorf("no such base image: %s", this.Base) 54 | } 55 | 56 | t, err := template.ParseFiles(tpl) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | out, err := os.Create(fmt.Sprintf("%s/Dockerfile", workDir)) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | defer out.Close() 67 | 68 | return t.Execute(out, map[string]interface{}{ 69 | "Registry": g.Registry, 70 | "Tarball": tarball, 71 | "AppDir": "/opt/app", 72 | }) 73 | } 74 | 75 | func (this *Build) DockerBuild(workDir string) { 76 | this.UpdateBuildStatus("docker building...") 77 | logFile := fmt.Sprintf("%s/%d.log", g.LogDir, this.Id) 78 | cmd := exec.Command(g.BuildScript, workDir, this.UserName, this.App, this.Version, g.Registry, logFile) 79 | err := cmd.Start() 80 | if err != nil { 81 | this.UpdateBuildStatus(fmt.Sprintf("error occur when build: %v", err)) 82 | log.Printf("start cmd fail: %v when docker build %s/Dockerfile", err, workDir) 83 | return 84 | } 85 | 86 | err, timeout := systool.CmdRunWithTimeout(cmd, g.BuildTimeout) 87 | 88 | if err != nil { 89 | log.Printf("docker build %s/Dockerfile fail: %v", workDir, err) 90 | } 91 | 92 | if timeout { 93 | log.Printf("docker build %s/Dockerfile timeout", workDir) 94 | } 95 | 96 | if !timeout && err == nil { 97 | this.UpdateBuildStatus("successfully:-)") 98 | image := fmt.Sprintf("%s/%s/%s:%s", g.Registry, this.UserName, this.App, this.Version) 99 | this.UpdateImage(image) 100 | f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0600) 101 | if err == nil { 102 | defer f.Close() 103 | f.WriteString("\ndone:-)\n") 104 | } 105 | os.RemoveAll(workDir) 106 | } else { 107 | this.UpdateBuildStatus(fmt.Sprintf("is_timeout: %v, err: %v", timeout, err)) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /g/g.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego" 6 | "github.com/astaxie/beego/cache" 7 | "github.com/astaxie/beego/orm" 8 | _ "github.com/go-sql-driver/mysql" 9 | filetool "github.com/toolkits/file" 10 | "log" 11 | "runtime" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var ( 17 | Debug bool 18 | TmpDir string 19 | LogDir string 20 | BuildTimeout time.Duration 21 | UicInternal string 22 | UicExternal string 23 | Registry string 24 | Cache cache.Cache 25 | BuildScript string 26 | TplMapping map[string]string = make(map[string]string) 27 | Token string 28 | ) 29 | 30 | func init() { 31 | runtime.GOMAXPROCS(runtime.NumCPU()) 32 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 33 | ParseConfig() 34 | } 35 | 36 | func ParseConfig() { 37 | 38 | runMode := beego.AppConfig.String("runmode") 39 | if runMode == "dev" { 40 | Debug = true 41 | } else { 42 | Debug = false 43 | } 44 | 45 | TmpDir = beego.AppConfig.String("tmpdir") 46 | if TmpDir == "" { 47 | log.Fatalln("configuration tmpdir is blank") 48 | } 49 | 50 | err := filetool.InsureDir(TmpDir) 51 | if err != nil { 52 | log.Fatalf("create dir: %s fail: %v", TmpDir, err) 53 | } 54 | 55 | TmpDir, err = filetool.RealPath(TmpDir) 56 | if err != nil { 57 | log.Fatalf("get real path of %s fail: %v", TmpDir, err) 58 | } 59 | 60 | LogDir = beego.AppConfig.String("logdir") 61 | if LogDir == "" { 62 | log.Fatalln("configuration logdir is blank") 63 | } 64 | 65 | err = filetool.InsureDir(LogDir) 66 | if err != nil { 67 | log.Fatalf("create dir: %s fail: %v", LogDir, err) 68 | } 69 | 70 | LogDir, err = filetool.RealPath(LogDir) 71 | if err != nil { 72 | log.Fatalf("get real path of %s fail: %v", LogDir, err) 73 | } 74 | 75 | Token = beego.AppConfig.String("token") 76 | 77 | UicInternal = beego.AppConfig.String("uicinternal") 78 | if UicInternal == "" { 79 | log.Fatalln("configuration uicinternal is blank") 80 | } 81 | 82 | UicExternal = beego.AppConfig.String("uicexternal") 83 | if UicExternal == "" { 84 | log.Fatalln("configuration uicexternal is blank") 85 | } 86 | 87 | BuildScript = beego.AppConfig.String("buildscript") 88 | if BuildScript == "" { 89 | log.Fatalln("configuration buildscript is blank") 90 | } 91 | 92 | Registry = beego.AppConfig.String("registry") 93 | if Registry == "" { 94 | log.Fatalln("configuration registry is blank") 95 | } 96 | 97 | _buildTimeout, err := beego.AppConfig.Int64("buildtimeout") 98 | if err != nil { 99 | log.Fatalf("parse configuration buildtimeout fail: %v", err) 100 | } 101 | 102 | BuildTimeout = time.Duration(_buildTimeout) * time.Minute 103 | 104 | // tpl mapping 105 | tpl_mapping := beego.AppConfig.String("tplmapping") 106 | tpl_mapping = strings.TrimSpace(tpl_mapping) 107 | mappings := strings.Split(tpl_mapping, ",") 108 | for i := 0; i < len(mappings); i++ { 109 | _mappings := strings.TrimSpace(mappings[i]) 110 | kv := strings.Split(_mappings, "=>") 111 | if len(kv) != 2 { 112 | log.Fatalf("split %s fail", _mappings) 113 | } 114 | 115 | TplMapping[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) 116 | } 117 | 118 | // cache 119 | Cache, err = cache.NewCache("memory", `{"interval":60}`) 120 | if err != nil { 121 | log.Fatalln("start cache fail :-(") 122 | } 123 | 124 | // db 125 | dbuser := beego.AppConfig.String("dbuser") 126 | dbpass := beego.AppConfig.String("dbpass") 127 | dbhost := beego.AppConfig.String("dbhost") 128 | dbport := beego.AppConfig.String("dbport") 129 | dbname := beego.AppConfig.String("dbname") 130 | dblink := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8", dbuser, dbpass, dbhost, dbport, dbname) 131 | // dblink = "root:1234@/uic?charset=utf8&loc=Asia%2FChongqing" 132 | 133 | orm.RegisterDriver("mysql", orm.DR_MySQL) 134 | orm.RegisterDataBase("default", "mysql", dblink+"&loc=Asia%2FChongqing", 30, 200) 135 | // orm.DefaultTimeLoc = time.UTC 136 | 137 | if Debug { 138 | orm.Debug = true 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /routers/index.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "github.com/dinp/builder/g" 7 | "github.com/dinp/builder/models" 8 | filetool "github.com/toolkits/file" 9 | str_ "github.com/toolkits/str" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | type MainController struct { 16 | PageController 17 | } 18 | 19 | func (this *MainController) Get() { 20 | this.Data["Mappings"] = g.TplMapping 21 | this.Layout = "layout.html" 22 | this.TplNames = "index.html" 23 | } 24 | 25 | func (this *MainController) Build() { 26 | o := &models.Build{} 27 | o.App = this.GetString("app") 28 | o.Version = this.GetString("version") 29 | o.Resume = this.GetString("resume") 30 | o.Base = this.GetString("base") 31 | o.Tarball = this.GetString("tarball") 32 | 33 | defer func() { 34 | this.Data["Mappings"] = g.TplMapping 35 | this.Data["O"] = o 36 | this.Layout = "layout.html" 37 | this.TplNames = "index.html" 38 | }() 39 | 40 | if o.App == "" { 41 | this.Data["Msg"] = "app名称不能为空" 42 | return 43 | } 44 | 45 | // app should be a-zA-Z0-9_- 46 | var appPattern = regexp.MustCompile(`^[a-zA-Z_]+[a-zA-Z0-9\-\_]*$`) 47 | if !appPattern.MatchString(o.App) { 48 | this.Data["Msg"] = "app名称应该符合正则:/^[a-zA-Z_]+[a-zA-Z0-9\\-\\_]*$/" 49 | return 50 | } 51 | 52 | if o.Version == "" { 53 | this.Data["Msg"] = "版本不能为空" 54 | return 55 | } 56 | 57 | // version should be digit and . 58 | var versionPattern = regexp.MustCompile(`^[0-9]+[0-9\.]*$`) 59 | if !versionPattern.MatchString(o.Version) { 60 | this.Data["Msg"] = "version应该符合正则:/^[0-9]+[0-9\\.]*$/" 61 | return 62 | } 63 | 64 | if o.Base == "" { 65 | this.Data["Msg"] = "Base Image不能为空" 66 | return 67 | } 68 | 69 | workDir := fmt.Sprintf("%s/%s", g.TmpDir, str_.RandSeq(6)) 70 | err := filetool.InsureDir(workDir) 71 | if err != nil { 72 | this.Data["Msg"] = fmt.Sprintf("create temp dir fail: %v", err) 73 | return 74 | } 75 | 76 | var fileName, filePath string 77 | 78 | if o.Tarball == "" { 79 | _, header, err := this.GetFile("file") 80 | if err != nil { 81 | this.Data["Msg"] = err.Error() 82 | return 83 | } 84 | 85 | // handle upload file 86 | fileName = header.Filename 87 | filePath = fmt.Sprintf("%s/%s", workDir, fileName) 88 | err = this.SaveToFile("file", filePath) 89 | if err != nil { 90 | this.Data["Msg"] = fmt.Sprintf("save file fail: %v", err) 91 | return 92 | } 93 | } else { 94 | if !strings.HasPrefix(o.Tarball, "http://") { 95 | this.Data["Msg"] = "tarball地址应该是一个http地址" 96 | return 97 | } 98 | 99 | if !(strings.HasSuffix(o.Tarball, ".tar.gz") || strings.HasSuffix(o.Tarball, ".war")) { 100 | this.Data["Msg"] = "tarball地址应该以.tar.gz或.war结尾" 101 | return 102 | } 103 | 104 | idx := strings.LastIndex(o.Tarball, "/") 105 | fileName = o.Tarball[idx+1:] 106 | filePath = fmt.Sprintf("%s/%s", workDir, fileName) 107 | 108 | err = filetool.Download(filePath, o.Tarball) 109 | if err != nil { 110 | this.Data["Msg"] = fmt.Sprintf("download tarball fail: %v", err) 111 | return 112 | } 113 | } 114 | 115 | o.UserId = this.CurrentUser.Id 116 | o.UserName = this.CurrentUser.Name 117 | o.Status = "saved meta in db" 118 | _, err = orm.NewOrm().Insert(o) 119 | if err != nil { 120 | this.Data["Msg"] = fmt.Sprintf("save meta to db fail: %v", err) 121 | return 122 | } 123 | 124 | err = o.GenDockerfile(workDir, fileName) 125 | if err != nil { 126 | this.Data["Msg"] = fmt.Sprintf("generate Dockerfile fail: %v", err) 127 | return 128 | } 129 | 130 | go o.DockerBuild(workDir) 131 | 132 | this.Redirect(fmt.Sprintf("/progress/%d", o.Id), 302) 133 | } 134 | 135 | func (this *MainController) Progress() { 136 | idStr := this.Ctx.Input.Param(":id") 137 | this.Data["Id"] = idStr 138 | 139 | buildId, err := strconv.ParseInt(idStr, 10, 64) 140 | build := models.Build{Id: buildId} 141 | err = orm.NewOrm().Read(&build) 142 | if err != nil { 143 | this.ServeErrJson(err.Error()) 144 | return 145 | } 146 | 147 | this.Data["Image"] = fmt.Sprintf("%s/%s/%s:%s", g.Registry, build.UserName, build.App, build.Version) 148 | this.Layout = "layout.html" 149 | this.TplNames = "progress.html" 150 | } 151 | 152 | func (this *MainController) Log() { 153 | idStr := this.Ctx.Input.Param(":id") 154 | buildId, err := strconv.ParseInt(idStr, 10, 64) 155 | build := models.Build{Id: buildId} 156 | err = orm.NewOrm().Read(&build) 157 | if err != nil { 158 | this.ServeErrJson(err.Error()) 159 | return 160 | } 161 | 162 | content, err := filetool.ToString(fmt.Sprintf("%s/%d.log", g.LogDir, buildId)) 163 | if err != nil { 164 | this.ServeErrJson(err.Error()) 165 | return 166 | } 167 | 168 | content = strings.Replace(content, "\n", "
", -1) 169 | 170 | this.ServeDataJson(map[string]interface{}{"build": build, "log": content}) 171 | } 172 | 173 | func (this *MainController) History() { 174 | this.Data["BuildHistory"] = models.BuildHistory(this.CurrentUser.Id) 175 | this.Layout = "layout.html" 176 | this.TplNames = "history.html" 177 | } 178 | 179 | func (this *MainController) DeleteHistory() { 180 | id, err := this.GetInt("id") 181 | if err != nil { 182 | this.ServeErrJson("id invalid") 183 | return 184 | } 185 | 186 | err = models.DeleteHistory(int64(id), this.CurrentUser.Id) 187 | if err != nil { 188 | this.ServeErrJson(err.Error()) 189 | } else { 190 | this.ServeOKJson() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /static/layer/extend/layer.ext.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @Name: layer拓展类,依赖于layer 4 | @Date: 2014.08.13 5 | @Author: 贤心 6 | @Versions:1.8.5-ext 7 | @Api:http://sentsin.com/jquery/layer 8 | @Desc: 本拓展会持续更新 9 | 10 | */ 11 | 12 | layer.use("skin/layer.ext.css",function(){layer.ext&&layer.ext()}),layer.prompt=function(a,b,c){var d={},a=a||{},e={area:["auto","auto"],offset:[a.top||"",""],title:a.title||"信息",dialog:{btns:2,type:-1,msg:'',yes:function(c){var e=d.prompt.val();""===e?d.prompt.focus():e.replace(/\s/g,"").length>(a.length||1e3)?layer.tips("最多输入"+(a.length||1e3)+"个字数","#xubox_prompt",2):b&&b(e,c,d.prompt)},no:c},success:function(){d.prompt=$("#xubox_prompt"),d.prompt.focus()}};return 3===a.type&&(e.dialog.msg='"),$.layer(e)},layer.tab=function(a){var a=a||{},c=a.data||[],d={type:1,border:[0],area:["auto","auto"],bgcolor:"",title:!1,shade:a.shade,offset:a.offset,move:".xubox_tabmove",closeBtn:!1,page:{html:'
'}()+''+'
'+function(){var a=c.length,b=1,d="";if(a>0)for(d=''+c[0].title+"";a>b;b++)d+=""+c[b].title+"";return d}()+"
"+'"+'X'+"
"},success:function(a){var b=$(".xubox_tabtit").children(),c=$(".xubox_tab_main").children(),d=$(".xubox_tabclose");b.on("click",function(){var a=$(this),b=a.index();a.addClass("xubox_tabnow").siblings().removeClass("xubox_tabnow"),c.eq(b).show().siblings().hide()}),d.on("click",function(){layer.close(a.attr("times"))})}};return $.layer(d)},layer.photos=function(a){var b,c,d,e,f,g,h,i;if(a=a||{},b={imgIndex:1,end:null,html:$("html")},c=$(window),d=a.json,e=a.page,d){if(f=d.data,1!==d.status)return layer.msg("未请求到数据",2,8),void 0;if(b.imgLen=f.length,!(f.length>0))return layer.msg("没有任何图片",2,8),void 0;b.thissrc=f[d.start].src,b.pid=f[d.start].pid,b.imgsname=d.title||"",b.name=f[d.start].name,b.imgIndex=d.start+1}else g=$(e.parent).find("img"),h=g.eq(e.start),b.thissrc=h.attr("layer-img")||h.attr("src"),b.pid=h.attr("pid"),b.imgLen=g.length,b.imgsname=e.title||"",b.name=h.attr("alt"),b.imgIndex=e.start+1;return i={type:1,border:[0],area:[(a.html?915:600)+"px","auto"],title:!1,shade:[.9,"#000",!0],shadeClose:!0,offset:["25px",""],bgcolor:"",page:{html:'
'+(b.name||
'+function(){return b.imgLen>1?'':""}()+'
'+b.imgsname+" "+b.imgIndex+"/"+b.imgLen+"
"+function(){return a.html?'
'+a.html+"
":""}()},success:function(a){b.bigimg=a.find(".xubox_bigimg"),b.imgsee=b.bigimg.find(".xubox_imgsee"),b.imgbar=b.imgsee.find(".xubox_imgbar"),b.imgtit=b.imgbar.find(".xubox_imgtit"),b.layero=a;var c=b.imgs=b.bigimg.find("img");clearTimeout(b.timerr),b.timerr=setTimeout(function(){$("html").css("overflow","hidden").attr("layer-full",b.index)},10),c.load(function(){b.imgarea=[c.outerWidth(),c.outerHeight()],b.resize(a)}),b.event()},end:function(){layer.closeAll(),b.end=!0}},b.event=function(){b.bigimg.hover(function(){b.imgsee.show()},function(){b.imgsee.hide()}),i.imgprev=function(){b.imgIndex--,b.imgIndex<1&&(b.imgIndex=b.imgLen),b.tabimg()},b.bigimg.find(".xubox_prev").on("click",function(a){a.preventDefault(),i.imgprev()}),i.imgnext=function(){b.imgIndex++,b.imgIndex>b.imgLen&&(b.imgIndex=1),b.tabimg()},b.bigimg.find(".xubox_next").on("click",function(a){a.preventDefault(),i.imgnext()}),$(document).keyup(function(a){if(!b.end){var c=a.keyCode;a.preventDefault(),37===c?i.imgprev():39===c?i.imgnext():27===c&&layer.close(b.index)}}),b.tabimg=function(){var e,h,i,j,k;b.imgs.removeAttr("style"),d?(j=f[b.imgIndex-1],e=j.src,h=j.pid,i=j.name):(k=g.eq(b.imgIndex-1),e=k.attr("layer-img")||k.attr("src"),h=k.attr("layer-pid")||"",i=k.attr("alt")||""),b.imgs.attr({src:e,"layer-pid":h,alt:i}),b.imgtit.find("em").text(b.imgIndex+"/"+b.imgLen),b.imgsee.show(),a.tab&&a.tab({pid:h,name:i})}},b.resize=function(d){var g,e={},f=[c.width(),c.height()];e.limit=f[0]-f[0]/f[1]*(60*f[0]/f[1]),e.limit<600&&(e.limit=600),g=[e.limit,f[1]>400?f[1]-50:400],g[0]=a.html?g[0]:g[0]-300,layer.area(b.index,{width:g[0]+(a.html?15:0),height:g[1]}),e.flwidth=g[0]-(a.html?300:0),b.imgarea[0]>e.flwidth?b.imgs.css({width:e.flwidth}):b.imgs.css({width:b.imgarea[0]}),b.imgs.outerHeight()-element the Select2-plugin may be run against - 9 | * are copied to the .select2-container. 10 | * 11 | * 1. Overwrite .select2-container's original display:inline-block 12 | * with Bootstrap 3's default for .form-control, display:block; 13 | * courtesy of @juristr (@see https://github.com/fk/select2-bootstrap-css/pull/1) 14 | */ 15 | .select2-container.form-control { 16 | background: transparent; 17 | border: none; 18 | display: block; 19 | /* 1 */ 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | /** 25 | * Adjust Select2 inputs to fit Bootstrap 3 default .form-control appearance. 26 | */ 27 | .select2-container .select2-choices .select2-search-field input, 28 | .select2-container .select2-choice, 29 | .select2-container .select2-choices { 30 | background: none; 31 | padding: 0; 32 | border-color: #cccccc; 33 | border-radius: 4px; 34 | color: #555555; 35 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 36 | background-color: white; 37 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 38 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 39 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 40 | } 41 | 42 | .select2-search input { 43 | border-color: #cccccc; 44 | border-radius: 4px; 45 | color: #555555; 46 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 47 | background-color: white; 48 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 49 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 50 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 51 | } 52 | 53 | .select2-container .select2-choices .select2-search-field input { 54 | -webkit-box-shadow: none; 55 | box-shadow: none; 56 | } 57 | 58 | /** 59 | * Adjust Select2 input heights to match the Bootstrap default. 60 | */ 61 | .select2-container .select2-choice { 62 | height: 34px; 63 | line-height: 1.42857; 64 | } 65 | 66 | /** 67 | * Address Multi Select2's height which - depending on how many elements have been selected - 68 | * may grown higher than their initial size. 69 | */ 70 | .select2-container.select2-container-multi.form-control { 71 | height: auto; 72 | } 73 | 74 | /** 75 | * Address Bootstrap 3 control sizing classes 76 | * @see http://getbootstrap.com/css/#forms-control-sizes 77 | */ 78 | .select2-container.input-sm .select2-choice, 79 | .input-group-sm .select2-container .select2-choice { 80 | height: 30px; 81 | line-height: 1.5; 82 | border-radius: 3px; 83 | } 84 | 85 | .select2-container.input-lg .select2-choice, 86 | .input-group-lg .select2-container .select2-choice { 87 | height: 45px; 88 | line-height: 1.33; 89 | border-radius: 6px; 90 | } 91 | 92 | .select2-container-multi .select2-choices .select2-search-field input { 93 | height: 32px; 94 | } 95 | 96 | .select2-container-multi.input-sm .select2-choices .select2-search-field input, 97 | .input-group-sm .select2-container-multi .select2-choices .select2-search-field input { 98 | height: 28px; 99 | } 100 | 101 | .select2-container-multi.input-lg .select2-choices .select2-search-field input, 102 | .input-group-lg .select2-container-multi .select2-choices .select2-search-field input { 103 | height: 43px; 104 | } 105 | 106 | /** 107 | * Adjust height and line-height for .select2-search-field amd multi-select Select2 widgets. 108 | * 109 | * 1. Class repetition to address missing .select2-chosen in Select2 < 3.3.2. 110 | */ 111 | .select2-container-multi .select2-choices .select2-search-field input { 112 | margin: 0; 113 | } 114 | 115 | .select2-chosen, 116 | .select2-choice > span:first-child, 117 | .select2-container .select2-choices .select2-search-field input { 118 | padding: 6px 12px; 119 | } 120 | 121 | .input-sm .select2-chosen, 122 | .input-group-sm .select2-chosen, 123 | .input-sm .select2-choice > span:first-child, 124 | .input-group-sm .select2-choice > span:first-child, 125 | .input-sm .select2-choices .select2-search-field input, 126 | .input-group-sm .select2-choices .select2-search-field input { 127 | padding: 5px 10px; 128 | } 129 | 130 | .input-lg .select2-chosen, 131 | .input-group-lg .select2-chosen, 132 | .input-lg .select2-choice > span:first-child, 133 | .input-group-lg .select2-choice > span:first-child, 134 | .input-lg .select2-choices .select2-search-field input, 135 | .input-group-lg .select2-choices .select2-search-field input { 136 | padding: 10px 16px; 137 | } 138 | 139 | .select2-container-multi .select2-choices .select2-search-choice { 140 | margin-top: 5px; 141 | margin-bottom: 3px; 142 | } 143 | 144 | .select2-container-multi.input-sm .select2-choices .select2-search-choice, 145 | .input-group-sm .select2-container-multi .select2-choices .select2-search-choice { 146 | margin-top: 3px; 147 | margin-bottom: 2px; 148 | } 149 | 150 | .select2-container-multi.input-lg .select2-choices .select2-search-choice, 151 | .input-group-lg .select2-container-multi .select2-choices .select2-search-choice { 152 | line-height: 24px; 153 | } 154 | 155 | /** 156 | * Adjust the single Select2's dropdown arrow button appearance. 157 | * 158 | * 1. For Select2 v.3.3.2. 159 | */ 160 | .select2-container .select2-choice .select2-arrow, 161 | .select2-container .select2-choice div { 162 | border-left: 1px solid #cccccc; 163 | background: none; 164 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 165 | } 166 | 167 | .select2-dropdown-open .select2-choice .select2-arrow, 168 | .select2-dropdown-open .select2-choice div { 169 | border-left-color: transparent; 170 | background: none; 171 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 172 | } 173 | 174 | /** 175 | * Adjust the dropdown arrow button icon position for the single-select Select2 elements 176 | * to make it line up vertically now that we increased the height of .select2-container. 177 | * 178 | * 1. Class repetition to address missing .select2-chosen in Select2 v.3.3.2. 179 | */ 180 | .select2-container .select2-choice .select2-arrow b, 181 | .select2-container .select2-choice div b { 182 | background-position: 0 3px; 183 | } 184 | 185 | .select2-dropdown-open .select2-choice .select2-arrow b, 186 | .select2-dropdown-open .select2-choice div b { 187 | background-position: -18px 3px; 188 | } 189 | 190 | .select2-container.input-sm .select2-choice .select2-arrow b, 191 | .input-group-sm .select2-container .select2-choice .select2-arrow b, 192 | .select2-container.input-sm .select2-choice div b, 193 | .input-group-sm .select2-container .select2-choice div b { 194 | background-position: 0 1px; 195 | } 196 | 197 | .select2-dropdown-open.input-sm .select2-choice .select2-arrow b, 198 | .input-group-sm .select2-dropdown-open .select2-choice .select2-arrow b, 199 | .select2-dropdown-open.input-sm .select2-choice div b, 200 | .input-group-sm .select2-dropdown-open .select2-choice div b { 201 | background-position: -18px 1px; 202 | } 203 | 204 | .select2-container.input-lg .select2-choice .select2-arrow b, 205 | .input-group-lg .select2-container .select2-choice .select2-arrow b, 206 | .select2-container.input-lg .select2-choice div b, 207 | .input-group-lg .select2-container .select2-choice div b { 208 | background-position: 0 9px; 209 | } 210 | 211 | .select2-dropdown-open.input-lg .select2-choice .select2-arrow b, 212 | .input-group-lg .select2-dropdown-open .select2-choice .select2-arrow b, 213 | .select2-dropdown-open.input-lg .select2-choice div b, 214 | .input-group-lg .select2-dropdown-open .select2-choice div b { 215 | background-position: -18px 9px; 216 | } 217 | 218 | /** 219 | * Address Bootstrap's validation states and change Select2's border colors and focus states. 220 | * Apply .has-warning, .has-danger or .has-succes to #select2-drop to match Bootstraps' colors. 221 | */ 222 | .has-warning .select2-choice, 223 | .has-warning .select2-choices { 224 | border-color: #c09853; 225 | } 226 | .has-warning .select2-container-active .select2-choice, 227 | .has-warning .select2-container-multi.select2-container-active .select2-choices { 228 | border-color: #a47e3c; 229 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; 230 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; 231 | } 232 | .has-warning.select2-drop-active { 233 | border-color: #a47e3c; 234 | } 235 | .has-warning.select2-drop-active.select2-drop.select2-drop-above { 236 | border-top-color: #a47e3c; 237 | } 238 | 239 | .has-error .select2-choice, 240 | .has-error .select2-choices { 241 | border-color: #b94a48; 242 | } 243 | .has-error .select2-container-active .select2-choice, 244 | .has-error .select2-container-multi.select2-container-active .select2-choices { 245 | border-color: #953b39; 246 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; 247 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; 248 | } 249 | .has-error.select2-drop-active { 250 | border-color: #953b39; 251 | } 252 | .has-error.select2-drop-active.select2-drop.select2-drop-above { 253 | border-top-color: #953b39; 254 | } 255 | 256 | .has-success .select2-choice, 257 | .has-success .select2-choices { 258 | border-color: #468847; 259 | } 260 | .has-success .select2-container-active .select2-choice, 261 | .has-success .select2-container-multi.select2-container-active .select2-choices { 262 | border-color: #356635; 263 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; 264 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; 265 | } 266 | .has-success.select2-drop-active { 267 | border-color: #356635; 268 | } 269 | .has-success.select2-drop-active.select2-drop.select2-drop-above { 270 | border-top-color: #356635; 271 | } 272 | 273 | /** 274 | * Make Select2's active-styles - applied to .select2-container when the widget receives focus - 275 | * fit Bootstrap 3's .form-element:focus appearance. 276 | */ 277 | .select2-container-active .select2-choice, 278 | .select2-container-multi.select2-container-active .select2-choices { 279 | border-color: #66afe9; 280 | outline: none; 281 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 282 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 283 | -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 284 | transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 285 | } 286 | 287 | .select2-drop-active { 288 | border-color: #66afe9; 289 | } 290 | 291 | .select2-drop-auto-width, 292 | .select2-drop.select2-drop-above.select2-drop-active { 293 | border-top-color: #66afe9; 294 | } 295 | 296 | /** 297 | * Select2 widgets in Bootstrap Input Groups 298 | * 299 | * When Select2 widgets are combined with other elements using Bootstrap 3's 300 | * "Input Group" component, we don't want specific edges of the Select2 container 301 | * to have a border-radius. 302 | * 303 | * In Bootstrap 2, input groups required a markup where these style adjustments 304 | * could be bound to a CSS-class identifying if the additional elements are appended, 305 | * prepended or both. 306 | * 307 | * Bootstrap 3 doesn't rely on these classes anymore, so we have to use our own. 308 | * Use .select2-bootstrap-prepend and .select2-bootstrap-append on a Bootstrap 3 .input-group 309 | * to let the contained Select2 widget know which edges should not be rounded as they are 310 | * directly followed by another element. 311 | * 312 | * @see http://getbootstrap.com/components/#input-groups 313 | */ 314 | .input-group.select2-bootstrap-prepend [class^="select2-choice"] { 315 | border-bottom-left-radius: 0 !important; 316 | border-top-left-radius: 0 !important; 317 | } 318 | 319 | .input-group.select2-bootstrap-append [class^="select2-choice"] { 320 | border-bottom-right-radius: 0 !important; 321 | border-top-right-radius: 0 !important; 322 | } 323 | 324 | .select2-dropdown-open [class^="select2-choice"] { 325 | border-bottom-right-radius: 0 !important; 326 | border-bottom-left-radius: 0 !important; 327 | } 328 | 329 | .select2-dropdown-open.select2-drop-above [class^="select2-choice"] { 330 | border-top-right-radius: 0 !important; 331 | border-top-left-radius: 0 !important; 332 | } 333 | 334 | /** 335 | * Adjust Select2's choices hover and selected styles to match Bootstrap 3's default dropdown styles. 336 | */ 337 | .select2-results .select2-highlighted { 338 | color: white; 339 | background-color: #428bca; 340 | } 341 | 342 | /** 343 | * Adjust alignment of Bootstrap 3 buttons in Bootstrap 3 Input Groups to address 344 | * Multi Select2's height which - depending on how many elements have been selected - 345 | * may grown higher than their initial size. 346 | */ 347 | .select2-bootstrap-append .select2-container-multiple, 348 | .select2-bootstrap-append .input-group-btn, 349 | .select2-bootstrap-append .input-group-btn .btn, 350 | .select2-bootstrap-prepend .select2-container-multiple, 351 | .select2-bootstrap-prepend .input-group-btn, 352 | .select2-bootstrap-prepend .input-group-btn .btn { 353 | vertical-align: top; 354 | } 355 | 356 | /** 357 | * Make Multi Select2's choices match Bootstrap 3's default button styles. 358 | */ 359 | .select2-container-multi .select2-choices .select2-search-choice { 360 | color: #555555; 361 | background: white; 362 | border-color: #cccccc; 363 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 364 | -webkit-box-shadow: none; 365 | box-shadow: none; 366 | } 367 | 368 | .select2-container-multi .select2-choices .select2-search-choice-focus { 369 | background: #ebebeb; 370 | border-color: #adadad; 371 | color: #333333; 372 | -webkit-box-shadow: none; 373 | box-shadow: none; 374 | } 375 | 376 | /** 377 | * Address Multi Select2's choice close-button vertical alignment. 378 | */ 379 | .select2-search-choice-close { 380 | margin-top: -7px; 381 | top: 50%; 382 | } 383 | 384 | /** 385 | * Adjust the single Select2's clear button position (used to reset the select box 386 | * back to the placeholder value and visible once a selection is made 387 | * activated by Select2's "allowClear" option). 388 | */ 389 | .select2-container .select2-choice abbr { 390 | top: 50%; 391 | } 392 | 393 | /** 394 | * Adjust "no results" and "selection limit" messages to make use 395 | * of Bootstrap 3's default "Alert" style. 396 | * 397 | * @see http://getbootstrap.com/components/#alerts-default 398 | */ 399 | .select2-results .select2-no-results, 400 | .select2-results .select2-searching, 401 | .select2-results .select2-selection-limit { 402 | background-color: #fcf8e3; 403 | color: #c09853; 404 | } 405 | 406 | /** 407 | * Address disabled Select2 styles. 408 | * 409 | * 1. For Select2 v.3.3.2. 410 | * 2. Revert border-left:0 inherited from Select2's CSS to prevent the arrow 411 | * from jumping when switching from disabled to enabled state and vice versa. 412 | */ 413 | .select2-container.select2-container-disabled .select2-choice, 414 | .select2-container.select2-container-disabled .select2-choices { 415 | cursor: not-allowed; 416 | background-color: #eeeeee; 417 | border-color: #cccccc; 418 | } 419 | .select2-container.select2-container-disabled .select2-choice .select2-arrow, 420 | .select2-container.select2-container-disabled .select2-choice div, 421 | .select2-container.select2-container-disabled .select2-choices .select2-arrow, 422 | .select2-container.select2-container-disabled .select2-choices div { 423 | background-color: transparent; 424 | border-left: 1px solid transparent; 425 | /* 2 */ 426 | } 427 | 428 | /** 429 | * Address Select2's loading indicator position - which should not stick 430 | * to the right edge of Select2's search input. 431 | * 432 | * 1. in .select2-search input 433 | * 2. in Multi Select2's .select2-search-field input 434 | * 3. in the status-message of infinite-scroll with remote data (@see http://ivaynberg.github.io/select2/#infinite) 435 | * 436 | * These styles alter Select2's default background-position of 100% 437 | * and supply the new background-position syntax to browsers which support it: 438 | * 439 | * 1. Android, Safari < 6/Mobile, IE<9: change to a relative background-position of 99% 440 | * 2. Chrome 25+, Firefox 13+, IE 9+, Opera 10.5+: use the new CSS3-background-position syntax 441 | * 442 | * @see http://www.w3.org/TR/css3-background/#background-position 443 | * 444 | * @todo Since both Select2 and Bootstrap 3 only support IE8 and above, 445 | * we could use the :after-pseudo-element to display the loading indicator. 446 | * Alternatively, we could supply an altered loading indicator image which already 447 | * contains an offset to the right. 448 | */ 449 | .select2-search input.select2-active, 450 | .select2-container-multi .select2-choices .select2-search-field input.select2-active, 451 | .select2-more-results.select2-active { 452 | background-position: 99%; 453 | /* 4 */ 454 | background-position: right 4px center; 455 | /* 5 */ 456 | } 457 | -------------------------------------------------------------------------------- /static/layer/layer.min.js: -------------------------------------------------------------------------------- 1 | /**************************************** 2 | 3 | @Name:layer v1.8.5 弹层组件压缩版 4 | @Author:贤心 5 | @Date:2014-08-13 6 | @Blog:http://sentsin.com 7 | @Copyright:Sentsin Xu(贤心) 8 | @官网:http://sentsin.com/jquery/layer 9 | 10 | */ 11 | 12 | ;!function(a,b){ 13 | "use strict"; 14 | var d,e,g,h,i, 15 | c="", //组件存放目录,为空表示自动获取(不用填写host,相对站点的根目录即可)。 16 | f={host:"http://"+location.host,getPath:function(){var a=document.scripts,b=a[a.length-1].src;return c?f.host+c:b.substring(0,b.lastIndexOf("/")+1)},type:["dialog","page","iframe","loading","tips"]};a.layer={v:"1.8.5",ie6:!-[1,]&&!a.XMLHttpRequest,index:0,path:f.getPath(),use:function(a,b){var f,g,h,e=d("head")[0];a=a.replace(/\s/g,""),f=/\.css$/.test(a),g=document.createElement(f?"link":"script"),h=a.replace(/\.|\//g,""),f&&(g.type="text/css",g.rel="stylesheet"),g[f?"href":"src"]=/^http:\/\//.test(a)?a:layer.path+a,g.id=h,d("#"+h)[0]||e.appendChild(g),b&&(document.all?d(g).ready(b):d(g).load(b))},alert:function(a,b,c,e){var f="function"==typeof c,g={dialog:{msg:a,type:b,yes:f?c:e},area:["auto","auto"]};return f||(g.title=c),d.layer(g)},confirm:function(a,b,c,e){var f="function"==typeof c,g={dialog:{msg:a,type:4,btns:2,yes:b,no:f?c:e}};return f||(g.title=c),d.layer(g)},msg:function(a,c,e,f){var g={title:!1,closeBtn:!1,time:c===b?2:c,dialog:{msg:""===a||a===b?" ":a},end:f};return"object"==typeof e?(g.dialog.type=e.type,g.shade=e.shade,g.shift=e.rate):"function"==typeof e?g.end=e:g.dialog.type=e,d.layer(g)},load:function(a,b){return"string"==typeof a?layer.msg(a,b||0,16):d.layer({time:a,loading:{type:b},bgcolor:b?"#fff":"",shade:b?[.1,"#000"]:[0],border:3!==b&&b?[6,.3,"#000"]:[0],type:3,title:["",!1],closeBtn:[0,!1]})},tips:function(a,b,c,e,f,g){var h={type:4,shade:!1,success:function(a){this.closeBtn||a.find(".xubox_tips").css({"padding-right":10})},bgcolor:"",tips:{msg:a,follow:b}};return h.time="object"==typeof c?c.time:0|c,c=c||{},h.closeBtn=c.closeBtn||!1,h.maxWidth=c.maxWidth||e,h.tips.guide=c.guide||f,h.tips.style=c.style||g,h.tips.more=c.more,d.layer(h)}},g=["xubox_layer","xubox_iframe",".xubox_title",".xubox_text",".xubox_page",".xubox_main"],h=function(a){var b=this,c=b.config;layer.index++,b.index=layer.index,b.config=d.extend({},c,a),b.config.dialog=d.extend({},c.dialog,a.dialog),b.config.page=d.extend({},c.page,a.page),b.config.iframe=d.extend({},c.iframe,a.iframe),b.config.loading=d.extend({},c.loading,a.loading),b.config.tips=d.extend({},c.tips,a.tips),b.creat()},h.pt=h.prototype,h.pt.config={type:0,shade:[.3,"#000"],fix:!0,move:".xubox_title",title:"信息",offset:["","50%"],area:["310px","auto"],closeBtn:[0,!0],time:0,bgcolor:"#fff",border:[6,.3,"#000"],zIndex:19891014,maxWidth:400,dialog:{btns:1,btn:["确定","取消"],type:8,msg:"",yes:function(a){layer.close(a)},no:function(a){layer.close(a)}},page:{dom:"#xulayer",html:"",url:""},iframe:{src:"http://sentsin.com",scrolling:"auto"},loading:{type:0},tips:{msg:"",follow:"",guide:0,isGuide:!0,style:["background-color:#FF9900; color:#fff;","#FF9900"]},success:function(){},close:function(a){layer.close(a)},end:function(){}},h.pt.space=function(a){var c,d,e,f,h,i,j,k,l,m,n,o,p,b=this;return a=a||"",c=b.index,d=b.config,e=d.dialog,f=-1===e.type?"":'',h=['
'+f+''+e.msg+"
",'
'+a+"
",'','','
'+d.tips.msg+'
'],i="",j="",k=d.zIndex+c,l="z-index:"+k+"; background-color:"+d.shade[1]+"; opacity:"+d.shade[0]+"; filter:alpha(opacity="+100*d.shade[0]+");",d.shade[0]&&(i='
'),d.zIndex=k,m="",n="",o="z-index:"+(k-1)+"; background-color: "+d.border[2]+"; opacity:"+d.border[1]+"; filter:alpha(opacity="+100*d.border[1]+"); top:-"+d.border[0]+"px; left:-"+d.border[0]+"px;",d.border[0]&&(j='
'),!d.maxmin||1!==d.type&&2!==d.type||/^\d+%$/.test(d.area[0])&&/^\d+%$/.test(d.area[1])||(n=''),d.closeBtn[1]&&(n+=''),p="object"==typeof d.title,d.title&&(m='
'+(p?d.title[0]:d.title)+"
"),[i,'
'+'
'+h[d.type]+m+''+n+""+''+"
"+j+"
"]},h.pt.creat=function(){var k,l,m,a=this,b="",c=a.config,e=c.dialog,f=a.index,h=c.page,i=d("body"),j=function(c){var c=c||"";b=a.space(c),i.append(d(b[0]))};switch(c.type){case 0:c.title||(c.area=["auto","auto"]),d(".xubox_dialog")[0]&&layer.close(d(".xubox_dialog").parents("."+g[0]).attr("times"));break;case 1:if(""!==h.html)j('
'+h.html+"
"),i.append(d(b[1]));else if(""!==h.url)j('
'+h.html+"
"),i.append(d(b[1])),d.get(h.url,function(a){d("#xuboxPageHtml"+f).html(a.toString()),h.ok&&h.ok(a)});else{if(0!=d(h.dom).parents(g[4]).length)return;j(),d(h.dom).show().wrap(d(b[1]))}break;case 3:c.title=!1,c.area=["auto","auto"],c.closeBtn=["",!1],d(".xubox_loading")[0]&&layer.closeLoad();break;case 4:c.title=!1,c.area=["auto","auto"],c.fix=!1,c.border=[0],c.tips.more||layer.closeTips()}if(1!==c.type&&(j(),i.append(d(b[1]))),k=a.layerE=d("#"+g[0]+f),k.css({width:c.area[0],height:c.area[1]}),c.fix||k.css({position:"absolute"}),c.title&&(3!==c.type||4!==c.type))switch(l=0===c.type?e:c,m=k.find(".xubox_botton"),l.btn=c.btn||e.btn,l.btns){case 0:m.html("").hide();break;case 1:m.html(''+l.btn[0]+"");break;case 2:m.html(''+l.btn[0]+""+''+l.btn[1]+"")}"auto"===k.css("left")?(k.hide(),setTimeout(function(){k.show(),a.set(f)},500)):a.set(f),c.time<=0||a.autoclose(),a.callback()},f.fade=function(a,b,c){a.css({opacity:0}).animate({opacity:c},b)},h.pt.offset=function(){var a=this,b=a.config,c=a.layerE,d=c.outerHeight();a.offsetTop=""===b.offset[0]&&dc.maxWidth&&k.width(c.maxWidth),q.tipColor=c.tips.style[1],o[0]=k.outerWidth(),q.autoLeft=function(){q.left+o[0]-e.width()>0?(q.tipLeft=q.left+q.width-o[0],r.css({right:12,left:"auto"})):q.tipLeft=q.left},q.where=[function(){q.autoLeft(),q.tipTop=q.top-o[1]-10,r.removeClass("layerTipsB").addClass("layerTipsT").css({"border-right-color":q.tipColor})},function(){q.tipLeft=q.left+q.width+10,q.tipTop=q.top,r.removeClass("layerTipsL").addClass("layerTipsR").css({"border-bottom-color":q.tipColor})},function(){q.autoLeft(),q.tipTop=q.top+q.height+10,r.removeClass("layerTipsT").addClass("layerTipsB").css({"border-right-color":q.tipColor})},function(){q.tipLeft=q.left-o[0]+10,q.tipTop=q.top,r.removeClass("layerTipsR").addClass("layerTipsL").css({"border-bottom-color":q.tipColor})}],q.where[c.tips.guide](),0===c.tips.guide?q.top-(e.scrollTop()+o[1]+16)<0&&q.where[2]():1===c.tips.guide?e.width()-(q.left+q.width+o[0]+16)>0||q.where[3]():2===c.tips.guide?q.top-e.scrollTop()+q.height+o[1]+16-e.height()>0&&q.where[0]():3===c.tips.guide?o[0]+16-q.left>0&&q.where[1]():4===c.tips.guide,k.css({left:q.tipLeft,top:q.tipTop})}c.fadeIn&&(f.fade(k,c.fadeIn,1),f.fade(d("#xubox_shade"+a),c.fadeIn,c.shade[0])),c.fix&&""===c.offset[0]&&!c.shift&&e.on("resize",function(){k.css({top:(e.height()-k.outerHeight())/2})}),b.move()},h.pt.shift=function(a,b,c){var k,d=this,f=d.config,g=d.layerE,h=0,i=e.width(),j=e.height()+(f.fix?0:e.scrollTop());switch(h="50%"==f.offset[1]||""==f.offset[1]?g.outerWidth()/2:g.outerWidth(),k={t:{top:d.offsetTop},b:{top:j-g.outerHeight()-f.border[0]},cl:h+f.border[0],ct:-g.outerHeight(),cr:i-h-f.border[0]},a){case"left-top":g.css({left:k.cl,top:k.ct}).animate(k.t,b);break;case"top":g.css({top:k.ct}).animate(k.t,b);break;case"right-top":g.css({left:k.cr,top:k.ct}).animate(k.t,b);break;case"right-bottom":g.css({left:k.cr,top:j}).animate(c?k.t:k.b,b);break;case"bottom":g.css({top:j}).animate(c?k.t:k.b,b);break;case"left-bottom":g.css({left:k.cl,top:j}).animate(c?k.t:k.b,b);break;case"left":g.css({left:-g.outerWidth()}).animate({left:d.offsetLeft},b)}},h.pt.autoArea=function(a){var c,e,f,h,i,k,j,l,m,n,o,b=this;switch(a=a||b.index,c=b.config,e=c.page,f=d("#"+g[0]+a),h=f.find(g[2]),i=f.find(g[5]),j=c.title?h.innerHeight():0,l=0,"auto"===c.area[0]&&i.outerWidth()>=c.maxWidth&&f.css({width:c.maxWidth}),c.type){case 0:m=f.find(".xubox_botton>a"),k=f.find(g[3]).outerHeight()+20,m.length>0&&(l=m.outerHeight()+20);break;case 1:n=f.find(g[4]),k=d(e.dom).outerHeight(),"auto"===c.area[0]&&f.css({width:n.outerWidth()}),(""!==e.html||""!==e.url)&&(k=n.outerHeight());break;case 2:f.find("iframe").css({width:f.outerWidth(),height:f.outerHeight()-(c.title?h.innerHeight():0)});break;case 3:o=f.find(".xubox_loading"),k=o.outerHeight(),i.css({width:o.width()})}"auto"===c.area[1]&&i.css({height:j+k+l}),d("#xubox_border"+a).css({width:f.outerWidth()+2*c.border[0],height:f.outerHeight()+2*c.border[0]}),layer.ie6&&"auto"!==c.area[0]&&i.css({width:f.outerWidth()}),"50%"!==c.offset[1]&&""!=c.offset[1]||4===c.type?f.css({marginLeft:0}):f.css({marginLeft:-f.outerWidth()/2})},h.pt.move=function(){var a=this,b=a.config,c={setY:0,moveLayer:function(){var a;a=0==parseInt(c.layerE.css("margin-left"))?parseInt(c.move.css("left")):parseInt(c.move.css("left"))+-parseInt(c.layerE.css("margin-left")),"fixed"!==c.layerE.css("position")&&(a-=c.layerE.parent().offset().left,c.setY=0),c.layerE.css({left:a,top:parseInt(c.move.css("top"))-c.setY})}},f=a.layerE.find(b.move);b.move&&f.attr("move","ok"),b.move?f.css({cursor:"move"}):f.css({cursor:"auto"}),d(b.move).on("mousedown",function(a){if(a.preventDefault(),"ok"===d(this).attr("move")){c.ismove=!0,c.layerE=d(this).parents("."+g[0]);var f=c.layerE.offset().left,h=c.layerE.offset().top,i=c.layerE.width()-6,j=c.layerE.height()-6;d("#xubox_moves")[0]||d("body").append('
'),c.move=d("#xubox_moves"),b.moveType&&c.move.css({opacity:0}),c.moveX=a.pageX-c.move.position().left,c.moveY=a.pageY-c.move.position().top,"fixed"!==c.layerE.css("position")||(c.setY=e.scrollTop())}}),d(document).mousemove(function(a){var d,f,g,h;c.ismove&&(d=a.pageX-c.moveX,f=a.pageY-c.moveY,a.preventDefault(),b.moveOut||(c.setY=e.scrollTop(),g=e.width()-c.move.outerWidth()-b.border[0],h=b.border[0]+c.setY,dg&&(d=g),h>f&&(f=h),f>e.height()-c.move.outerHeight()-b.border[0]+c.setY&&(f=e.height()-c.move.outerHeight()-b.border[0]+c.setY)),c.move.css({left:d,top:f}),b.moveType&&c.moveLayer(),d=null,f=null,g=null,h=null)}).mouseup(function(){try{c.ismove&&(c.moveLayer(),c.move.remove()),c.ismove=!1}catch(a){c.ismove=!1}b.moveEnd&&b.moveEnd()})},h.pt.autoclose=function(){var a=this,b=a.config.time,c=function(){b--,0===b&&(layer.close(a.index),clearInterval(a.autotime))};a.autotime=setInterval(c,1e3)},f.config={end:{}},h.pt.callback=function(){var a=this,b=a.layerE,c=a.config,e=c.dialog;a.openLayer(),a.config.success(b),layer.ie6&&a.IE6(b),b.find(".xubox_close").on("click",function(){c.close(a.index),layer.close(a.index)}),b.find(".xubox_yes").on("click",function(){c.yes?c.yes(a.index):e.yes(a.index)}),b.find(".xubox_no").on("click",function(){c.no?c.no(a.index):e.no(a.index),layer.close(a.index)}),a.config.shadeClose&&d("#xubox_shade"+a.index).on("click",function(){layer.close(a.index)}),b.find(".xubox_min").on("click",function(){layer.min(a.index,c),c.min&&c.min(b)}),b.find(".xubox_max").on("click",function(){d(this).hasClass("xubox_maxmin")?(layer.restore(a.index),c.restore&&c.restore(b)):(layer.full(a.index,c),c.full&&c.full(b))}),f.config.end[a.index]=c.end},f.reselect=function(){d.each(d("select"),function(){var c=d(this);c.parents("."+g[0])[0]||1==c.attr("layer")&&d("."+g[0]).length<1&&c.removeAttr("layer").show(),c=null})},h.pt.IE6=function(a){var f,b=this,c=a.offset().top;f=b.config.fix?function(){a.css({top:e.scrollTop()+c})}:function(){a.css({top:c})},f(),e.scroll(f),d.each(d("select"),function(){var c=d(this);c.parents("."+g[0])[0]||"none"==c.css("display")||c.attr({layer:"1"}).hide(),c=null})},h.pt.openLayer=function(){var a=this;a.layerE,layer.autoArea=function(b){return a.autoArea(b)},layer.shift=function(b,c,d){a.shift(b,c,d)},layer.setMove=function(){return a.move()},layer.zIndex=a.config.zIndex,layer.setTop=function(a){var b=function(){layer.zIndex++,a.css("z-index",layer.zIndex+1)};return layer.zIndex=parseInt(a[0].style.zIndex),a.on("mousedown",b),layer.zIndex}},f.isauto=function(a,b,c){"auto"===b.area[0]&&(b.area[0]=a.outerWidth()),"auto"===b.area[1]&&(b.area[1]=a.outerHeight()),a.attr({area:b.area+","+c}),a.find(".xubox_max").addClass("xubox_maxmin")},f.rescollbar=function(a){g.html.attr("layer-full")==a&&(g.html[0].style.removeProperty?g.html[0].style.removeProperty("overflow"):g.html[0].style.removeAttribute("overflow"),g.html.removeAttr("layer-full"))},layer.getIndex=function(a){return d(a).parents("."+g[0]).attr("times")},layer.getChildFrame=function(a,b){return b=b||d("."+g[1]).parents("."+g[0]).attr("times"),d("#"+g[0]+b).find("."+g[1]).contents().find(a)},layer.getFrameIndex=function(a){return d(a?"#"+a:"."+g[1]).parents("."+g[0]).attr("times")},layer.iframeAuto=function(a){var b,c,e,f,h;a=a||d("."+g[1]).parents("."+g[0]).attr("times"),b=layer.getChildFrame("body",a).outerHeight(),c=d("#"+g[0]+a),e=c.find(g[2]),f=0,e&&(f=e.height()),c.css({height:b+f}),h=-parseInt(d("#xubox_border"+a).css("top")),d("#xubox_border"+a).css({height:b+2*h+f}),d("#"+g[1]+a).css({height:b})},layer.iframeSrc=function(a,b){d("#"+g[0]+a).find("iframe").attr("src",b)},layer.area=function(a,b){var j,c=[d("#"+g[0]+a),d("#xubox_border"+a)],e=c[0].attr("type"),h=c[0].find(g[5]),i=c[0].find(g[2]);(e===f.type[1]||e===f.type[2])&&(c[0].css(b),h.css({width:b.width,height:b.height}),e===f.type[2]&&(j=c[0].find("iframe"),j.css({width:b.width,height:i?b.height-i.innerHeight():b.height})),"0px"!==c[0].css("margin-left")&&(b.hasOwnProperty("top")&&c[0].css({top:b.top-(c[1][0]?parseFloat(c[1].css("top")):0)}),b.hasOwnProperty("left")&&c[0].css({left:b.left+c[0].outerWidth()/2-(c[1][0]?parseFloat(c[1].css("left")):0)}),c[0].css({marginLeft:-c[0].outerWidth()/2})),c[1][0]&&c[1].css({width:parseFloat(b.width)-2*parseFloat(c[1].css("left")),height:parseFloat(b.height)-2*parseFloat(c[1].css("top"))}))},layer.min=function(a,b){var c=d("#"+g[0]+a),e=[c.position().top,c.position().left+parseFloat(c.css("margin-left"))];f.isauto(c,b,e),layer.area(a,{width:180,height:35}),c.find(".xubox_min").hide(),"page"===c.attr("type")&&c.find(g[4]).hide(),f.rescollbar(a)},layer.restore=function(a){var b=d("#"+g[0]+a),c=b.attr("area").split(",");b.attr("type"),layer.area(a,{width:parseFloat(c[0]),height:parseFloat(c[1]),top:parseFloat(c[2]),left:parseFloat(c[3])}),b.find(".xubox_max").removeClass("xubox_maxmin"),b.find(".xubox_min").show(),"page"===b.attr("type")&&b.find(g[4]).show(),f.rescollbar(a)},layer.full=function(a,b){var i,c=d("#"+g[0]+a),h=2*b.border[0]||6,j=[c.position().top,c.position().left+parseFloat(c.css("margin-left"))];f.isauto(c,b,j),g.html.attr("layer-full")||g.html.css("overflow","hidden").attr("layer-full",a),clearTimeout(i),i=setTimeout(function(){layer.area(a,{top:"fixed"===c.css("position")?0:e.scrollTop(),left:"fixed"===c.css("position")?0:e.scrollLeft(),width:e.width()-h,height:e.height()-h})},100)},layer.title=function(a,b){var c=d("#"+g[0]+(b||layer.index)).find(".xubox_title>em");c.html(a)},layer.close=function(a){var h,b=d("#"+g[0]+a),c=b.attr("type"),e=d("#xubox_moves, #xubox_shade"+a);if(b[0]){if(c==f.type[1])if(b.find(".xuboxPageHtml")[0])b[0].innerHTML="",b.remove();else for(b.find(".xubox_setwin,.xubox_close,.xubox_botton,.xubox_title,.xubox_border").remove(),h=0;3>h;h++)b.find(".layer_pageContent").unwrap().hide();else b[0].innerHTML="",b.remove();e.remove(),layer.ie6&&f.reselect(),f.rescollbar(a),"function"==typeof f.config.end[a]&&f.config.end[a](),delete f.config.end[a]}},layer.closeLoad=function(){layer.close(d(".xubox_loading").parents("."+g[0]).attr("times"))},layer.closeTips=function(){layer.closeAll("tips")},layer.closeAll=function(a){d.each(d("."+g[0]),function(){var b=d(this),c=a?b.attr("type")===a:1;c&&layer.close(b.attr("times")),c=null})},f.run=function(){d=jQuery,e=d(a),g.html=d("html"),layer.use("skin/layer.css"),d.layer=function(a){var b=new h(a);return b.index},(new Image).src=layer.path+"skin/default/xubox_ico0.png"},i="../../init/jquery",a.seajs?define([i],function(a,b,c){f.run(),c.exports=layer}):f.run()}(window); -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('