Browse Source

1、实现网站首页
2、实现文档阅读
3、实现项目发布
4、实现用户权限

lifei6671 8 years ago
parent
commit
ad67558b80

+ 14 - 0
Dockerfile

@@ -0,0 +1,14 @@
+FROM golang:1.8.1-alpine
+
+
+ADD . /go/src/github.com/lifei6671/godoc
+
+
+WORKDIR /go/src/github.com/lifei6671/godoc
+
+RUN chmod +x start.sh
+
+RUN  go build -ldflags "-w" && \
+    rm -rf commands controllers models routers search vendor .gitignore .travis.yml Dockerfile gide.yaml LICENSE main.go README.md utils graphics Godeps
+
+CMD ["./start.sh"]

+ 8 - 2
commands/command.go

@@ -4,13 +4,15 @@ import (
 	"fmt"
 	"net/url"
 	"time"
+	"os"
+	"encoding/gob"
 
 	"github.com/lifei6671/godoc/models"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/orm"
 	"github.com/astaxie/beego/logs"
-	"os"
 	"github.com/lifei6671/godoc/conf"
+
 )
 
 // RegisterDataBase 注册数据库
@@ -75,7 +77,7 @@ func RegisterLogger()  {
 	logs.EnableFuncCallDepth(true)
 	logs.Async()
 
-	beego.BeeLogger.DelLogger("console")
+	//beego.BeeLogger.DelLogger("console")
 	beego.SetLogger("file",`{"filename":"logs/log.log"}`)
 	beego.SetLogFuncCall(true)
 	beego.BeeLogger.Async()
@@ -100,4 +102,8 @@ func RegisterCommand() {
 
 func RegisterFunction()  {
 	beego.AddFuncMap("config",models.GetOptionValue)
+}
+
+func init()  {
+	gob.Register(models.Member{})
 }

+ 6 - 0
conf/app.conf

@@ -5,6 +5,12 @@ sessionon = true
 sessionname = smart_webhook_id
 copyrequestbody = true
 
+#默认Session生成Key的秘钥
+beegoserversessionkey=123456
+#Session储存方式
+sessionprovider=file
+sessionproviderconfig=./logs
+
 #生成回调地址时完整的域名
 base_url = https://hook.iminho.me
 

+ 6 - 0
conf/app.conf.example

@@ -6,6 +6,12 @@ sessionon = true
 sessionname = smart_webhook_id
 copyrequestbody = true
 
+#默认Session生成Key的秘钥
+beegoserversessionkey=123456
+#Session储存方式
+sessionprovider=file
+sessionproviderconfig=./logs
+
 #生成回调地址时完整的域名
 base_url = https://hook.iminho.me
 

+ 3 - 0
controllers/account.go

@@ -77,3 +77,6 @@ func (c *AccountController) Logout(){
 	c.Redirect(beego.URLFor("AccountController.Login"),302)
 }
 
+func (c *AccountController) Captcha()  {
+
+}

+ 13 - 3
controllers/base.go

@@ -7,32 +7,42 @@ import (
 	"github.com/lifei6671/godoc/models"
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego"
+	"strings"
 )
 
 
 type BaseController struct {
 	beego.Controller
 	Member *models.Member
+	Option map[string]string
+	EnableAnonymous bool
 }
 
 // Prepare 预处理.
 func (c *BaseController) Prepare (){
 	c.Data["SiteName"] = "MinDoc"
 	c.Data["Member"] = models.Member{}
+	c.EnableAnonymous = false
+
 
 	if member,ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0{
 		c.Member = &member
 		c.Data["Member"] = c.Member
 	}else{
-		c.Member = models.NewMember()
-		c.Member.Find(1)
-		c.Data["Member"] = *c.Member
+		//c.Member = models.NewMember()
+		//c.Member.Find(1)
+		//c.Data["Member"] = *c.Member
 	}
 	c.Data["BaseUrl"] = c.Ctx.Input.Scheme() + "://" + c.Ctx.Request.Host
 
 	if options,err := models.NewOption().All();err == nil {
+		c.Option = make(map[string]string,len(options))
 		for _,item := range options {
 			c.Data[item.OptionName] = item.OptionValue
+			c.Option[item.OptionName] = item.OptionValue
+			if strings.EqualFold(item.OptionName,"ENABLE_ANONYMOUS") && item.OptionValue == "true" {
+				c.EnableAnonymous = true
+			}
 		}
 	}
 }

+ 22 - 1
controllers/book.go

@@ -499,7 +499,28 @@ func (c *BookController) Delete() {
 
 //发布项目
 func (c *BookController) Release() {
-	c.JsonResult(0,"ok")
+	c.Prepare()
+
+	identify := c.GetString("identify")
+	book ,err := models.NewBookResult().FindByIdentify(identify,c.Member.MemberId)
+
+	if err != nil {
+		if err == models.ErrPermissionDenied {
+			c.JsonResult(6001,"权限不足")
+		}
+		if err == orm.ErrNoRows {
+			c.JsonResult(6002,"项目不存在")
+		}
+		beego.Error(err)
+		c.JsonResult(6003,"未知错误")
+	}
+	if book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder && book.RoleId != conf.BookEditor{
+		c.JsonResult(6003,"权限不足")
+	}
+
+	go models.NewDocument().ReleaseContent(book.BookId)
+
+	c.JsonResult(0,"发布任务已推送到任务队列,稍后将在后台执行。")
 }
 
 func (c *BookController) SaveSort() {

+ 128 - 5
controllers/document.go

@@ -21,12 +21,135 @@ type DocumentController struct {
 	BaseController
 }
 
-func (p *DocumentController) Index()  {
-	p.TplName = "document/index.tpl"
+func isReadable (identify,token string,c *DocumentController) *models.BookResult {
+	book,err := models.NewBook().FindByFieldFirst("identify",identify)
+
+	if err != nil {
+		beego.Error(err)
+		c.Abort("500")
+	}
+	//如果文档是私有的
+	if book.PrivatelyOwned == 1 {
+
+		is_ok := false
+
+		if c.Member != nil{
+			_, err := models.NewRelationship().FindForRoleId(book.BookId, c.Member.MemberId)
+			if err == nil {
+				is_ok = true
+			}
+		}
+		if book.PrivateToken != "" && !is_ok {
+			//如果有访问的Token,并且该项目设置了访问Token,并且和用户提供的相匹配,则记录到Session中.
+			//如果用户未提供Token且用户登录了,则判断用户是否参与了该项目.
+			//如果用户未登录,则从Session中读取Token.
+			if token != "" && strings.EqualFold(token, book.PrivateToken) {
+				c.SetSession(identify, token)
+
+			}  else if token, ok := c.GetSession(identify).(string); !ok || !strings.EqualFold(token, book.PrivateToken) {
+				c.Abort("403")
+			}
+		}else{
+			c.Abort("403")
+		}
+
+	}
+	bookResult := book.ToBookResult()
+
+	if c.Member != nil {
+		rel ,err := models.NewRelationship().FindByBookIdAndMemberId(bookResult.BookId,c.Member.MemberId)
+
+		if err == nil {
+			bookResult.MemberId 		= rel.MemberId
+			bookResult.RoleId		= rel.RoleId
+			bookResult.RelationshipId	= rel.RelationshipId
+		}
+	}
+	return bookResult
+}
+
+func (c *DocumentController) Index()  {
+	c.Prepare()
+	identify := c.Ctx.Input.Param(":key")
+	token := c.GetString("token")
+
+	if identify == "" {
+		c.Abort("404")
+	}
+	bookResult := isReadable(identify,token,c)
+
+	c.TplName = "document/" + bookResult.Theme + "_read.tpl"
+
+	tree,err := models.NewDocument().CreateDocumentTreeForHtml(bookResult.BookId,0)
+
+	if err != nil {
+		beego.Error(err)
+		c.Abort("500")
+	}
+
+
+	c.Data["Model"] = bookResult
+	c.Data["Result"] = template.HTML(tree)
+	c.Data["Title"] = "概要"
+	c.Data["Content"] = bookResult.Description
 }
 
-func (p *DocumentController) Read() {
-	p.TplName = "document/kancloud.tpl"
+func (c *DocumentController) Read() {
+	c.Prepare()
+	identify := c.Ctx.Input.Param(":key")
+	token := c.GetString("token")
+	id :=  c.GetString(":id")
+
+	if identify == "" || id == ""{
+		c.Abort("404")
+	}
+	bookResult := isReadable(identify,token,c)
+
+	c.TplName = "document/" + bookResult.Theme + "_read.tpl"
+
+	doc := models.NewDocument()
+
+	if doc_id,err := strconv.Atoi(id);err == nil  {
+		doc,err = doc.Find(doc_id)
+		if err != nil {
+			beego.Error(err)
+			c.Abort("500")
+		}
+	}else{
+		doc,err = doc.FindByFieldFirst("identify",id)
+		if err != nil {
+			beego.Error(err)
+			c.Abort("500")
+		}
+	}
+
+	if doc.BookId != bookResult.BookId {
+		c.Abort("403")
+	}
+	if c.IsAjax() {
+		var data struct{
+			DocTitle string `json:"doc_title"`
+			Body string	`json:"body"`
+			Title string `json:"title"`
+		}
+		data.DocTitle = doc.DocumentName
+		data.Body = doc.Release
+		data.Title = doc.DocumentName + " - Powered by MinDoc"
+
+		c.JsonResult(0,"ok",data)
+	}
+
+	tree,err := models.NewDocument().CreateDocumentTreeForHtml(bookResult.BookId,doc.DocumentId)
+
+	if err != nil {
+		beego.Error(err)
+		c.Abort("500")
+	}
+
+	c.Data["Model"] = bookResult
+	c.Data["Result"] = template.HTML(tree)
+	c.Data["Title"] = doc.DocumentName
+	c.Data["Content"] = template.HTML(doc.Release)
 }
 
 func (c *DocumentController) Edit()  {
@@ -67,7 +190,7 @@ func (c *DocumentController) Edit()  {
 	c.Data["Result"] = template.JS("[]")
 
 	trees ,err := models.NewDocument().FindDocumentTree(bookResult.BookId)
-	beego.Info("",trees)
+
 	if err != nil {
 		beego.Error("FindDocumentTree => ", err)
 	}else{

+ 37 - 2
controllers/home.go

@@ -1,9 +1,44 @@
 package controllers
 
+import (
+	"github.com/astaxie/beego"
+	"github.com/lifei6671/godoc/models"
+	"github.com/lifei6671/godoc/utils"
+)
+
 type HomeController struct {
 	BaseController
 }
 
-func (p *HomeController) Index() {
-	p.TplName = "home/index.tpl"
+func (c *HomeController) Index() {
+	c.Prepare()
+	c.TplName = "home/index.tpl"
+	//如果没有开启匿名访问,则跳转到登录页面
+	if !c.EnableAnonymous && c.Member == nil {
+		c.Redirect(beego.URLFor("AccountController.Login"),302)
+	}
+	pageIndex,_ := c.GetInt("page",1)
+	pageSize := 18
+
+	member_id := 0
+
+	if c.Member != nil {
+		member_id = c.Member.MemberId
+	}
+	books,totalCount,err := models.NewBook().FindForHomeToPager(pageIndex,pageSize,member_id)
+
+	if err != nil {
+		beego.Error(err)
+		c.Abort("500")
+	}
+	if totalCount > 0 {
+		html := utils.GetPagerHtml(c.Ctx.Request.RequestURI, pageIndex, pageSize, totalCount)
+
+		c.Data["PageHtml"] = html
+	}else {
+		c.Data["PageHtml"] = ""
+	}
+
+	c.Data["Lists"] = books
+
 }

+ 11 - 2
controllers/manager.go

@@ -320,18 +320,27 @@ func (c *ManagerController) Setting() {
 	if !c.Member.IsAdministrator() {
 		c.Abort("403")
 	}
-	if c.Ctx.Input.IsPost() {
 
-	}
 	options,err := models.NewOption().All()
 
+	if c.Ctx.Input.IsPost() {
+		for _,item := range options {
+			item.OptionValue = c.GetString(item.OptionName)
+			item.InsertOrUpdate()
+		}
+		c.JsonResult(0,"ok")
+	}
+
 	if err != nil {
 		c.Abort("500")
 	}
+	c.Data["SITE_TITLE"] = c.Option["SITE_NAME"]
+
 	for _,item := range options {
 		c.Data[item.OptionName] = item
 	}
 
+
 }
 
 func (c *ManagerController) Comments()  {

+ 0 - 1
logs/README.md

@@ -1 +0,0 @@
-## 日志存放目录

+ 1 - 0
main.go

@@ -3,6 +3,7 @@ package main
 import (
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lifei6671/godoc/routers"
+	_ "github.com/garyburd/redigo/redis"
 	"github.com/astaxie/beego"
 	"github.com/lifei6671/godoc/commands"
 )

+ 77 - 6
models/book.go

@@ -6,6 +6,7 @@ import (
 	"github.com/astaxie/beego/orm"
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego/logs"
+	"strings"
 )
 
 // Book struct .
@@ -32,8 +33,10 @@ type Book struct {
 	// CommentStatus 评论设置的状态:open 为允许所有人评论,closed 为不允许评论, group_only 仅允许参与者评论 ,registered_only 仅允许注册者评论.
 	CommentStatus string	`orm:"column(comment_status);size(20);default(open)" json:"comment_status"`
 	CommentCount int	`orm:"column(comment_count);type(int)" json:"comment_count"`
-	Cover string 		`orm:"column();size(1000)" json:"cover"`
-
+	//封面地址
+	Cover string 		`orm:"column(cover);size(1000)" json:"cover"`
+	//主题风格
+	Theme string 		`orm:"columen(theme);size(255);default(default)" json:"theme"`
 	// CreateTime 创建时间 .
 	CreateTime time.Time	`orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"`
 	MemberId int		`orm:"column(member_id);size(100)" json:"member_id"`
@@ -109,23 +112,27 @@ func (m *Book) Update(cols... string) error  {
 	return err
 }
 
-func (m *Book) FindByField(field string,value interface{}) ([]Book,error)  {
+//根据指定字段查询结果集.
+func (m *Book) FindByField(field string,value interface{}) ([]*Book,error)  {
 	o := orm.NewOrm()
 
-	var books []Book
-	_,err := o.QueryTable(conf.GetDatabasePrefix() + m.TableName()).Filter(field,value).All(&books)
+	var books []*Book
+	_,err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,value).All(&books)
 
 	return books,err
 }
 
+//根据指定字段查询一个结果.
 func (m *Book) FindByFieldFirst(field string,value interface{})(*Book,error) {
 	o := orm.NewOrm()
 
-	err := o.QueryTable(conf.GetDatabasePrefix() + m.TableName()).Filter(field,value).One(m)
+	err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,value).One(m)
 
 	return m,err
 
 }
+
+//分页查询指定用户的项目
 func (m *Book) FindToPager(pageIndex, pageSize ,memberId int) (books []*BookResult,totalCount int,err error){
 
 	relationship := NewRelationship()
@@ -242,11 +249,75 @@ func (m *Book) ThoroughDeleteBook(id int) error {
 
 }
 
+func (m *Book) FindForHomeToPager(pageIndex, pageSize ,member_id int) (books []*BookResult,totalCount int,err error) {
+	o := orm.NewOrm()
+
+	offset := (pageIndex - 1) * pageSize
+	//如果是登录用户
+	if member_id > 0 {
+		sql1 := "SELECT COUNT(*) FROM md_books AS book LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ? WHERE relationship_id > 0 OR book.privately_owned = 0"
+
+		err = o.Raw(sql1,member_id).QueryRow(&totalCount)
+		if err != nil {
+			return
+		}
+		sql2 := `SELECT book.*,rel1.*,member.account AS create_name FROM md_books AS book
+			LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ?
+			LEFT JOIN md_relationship AS rel1 ON rel1.book_id = book.book_id AND rel1.role_id = 0
+			LEFT JOIN md_members AS member ON rel1.member_id = member.member_id
+			WHERE rel.relationship_id > 0 OR book.privately_owned = 0 ORDER BY order_index DESC ,book.book_id DESC LIMIT ?,?`
 
+		_,err = o.Raw(sql2,member_id,offset,pageSize).QueryRows(&books)
 
+		return
 
+	}else{
+		count,err1 := o.QueryTable(m.TableNameWithPrefix()).Filter("privately_owned",0).Count()
 
+		if err1 != nil {
+			err = err1
+			return
+		}
+		totalCount = int(count)
 
+		sql := `SELECT book.*,rel.*,member.account AS create_name FROM md_books AS book
+			LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0
+			LEFT JOIN md_members AS member ON rel.member_id = member.member_id
+			WHERE book.privately_owned = 0 ORDER BY order_index DESC ,book.book_id DESC LIMIT ?,?`
+
+		_,err = o.Raw(sql,offset,pageSize).QueryRows(&books)
+
+		return
+
+	}
+
+}
+
+func (book *Book) ToBookResult() *BookResult {
+
+	m := NewBookResult()
+
+	m.BookId 	 	= book.BookId
+	m.BookName 	 	= book.BookName
+	m.Identify 	 	= book.Identify
+	m.OrderIndex 	 	= book.OrderIndex
+	m.Description 	 	= strings.Replace(book.Description, "\r\n", "<br/>", -1)
+	m.PrivatelyOwned 	= book.PrivatelyOwned
+	m.PrivateToken 		= book.PrivateToken
+	m.DocCount 		= book.DocCount
+	m.CommentStatus 	= book.CommentStatus
+	m.CommentCount 		= book.CommentCount
+	m.CreateTime 		= book.CreateTime
+	m.ModifyTime 		= book.ModifyTime
+	m.Cover 		= book.Cover
+	m.Label 		= book.Label
+	m.Status 		= book.Status
+	m.Editor 		= book.Editor
+	m.Theme			= book.Theme
+
+
+	return m
+}
 
 
 

+ 5 - 18
models/book_result.go

@@ -2,8 +2,8 @@ package models
 
 import (
 	"time"
+
 	"github.com/astaxie/beego/orm"
-	"strings"
 	"github.com/astaxie/beego/logs"
 	"github.com/lifei6671/godoc/conf"
 )
@@ -23,6 +23,7 @@ type BookResult struct {
 	CreateName string 	`json:"create_name"`
 	ModifyTime time.Time	`json:"modify_time"`
 	Cover string            `json:"cover"`
+	Theme string		`json:"theme"`
 	Label string		`json:"label"`
 	MemberId int            `json:"member_id"`
 	Editor string           `json:"editor"`
@@ -39,6 +40,7 @@ func NewBookResult() *BookResult {
 	return &BookResult{}
 }
 
+
 // 根据项目标识查询项目以及指定用户权限的信息.
 func (m *BookResult) FindByIdentify(identify string,member_id int) (*BookResult,error) {
 	if identify == "" || member_id <= 0 {
@@ -77,24 +79,9 @@ func (m *BookResult) FindByIdentify(identify string,member_id int) (*BookResult,
 		return m, err
 	}
 
-	m.BookId 	 	= book.BookId
-	m.BookName 	 	= book.BookName
-	m.Identify 	 	= book.Identify
-	m.OrderIndex 	 	= book.OrderIndex
-	m.Description 	 	= strings.Replace(book.Description, "\r\n", "<br/>", -1)
-	m.PrivatelyOwned 	= book.PrivatelyOwned
-	m.PrivateToken 		= book.PrivateToken
-	m.DocCount 		= book.DocCount
-	m.CommentStatus 	= book.CommentStatus
-	m.CommentCount 		= book.CommentCount
-	m.CreateTime 		= book.CreateTime
-	m.CreateName 		= member.Account
-	m.ModifyTime 		= book.ModifyTime
-	m.Cover 		= book.Cover
-	m.Label 		= book.Label
-	m.Status 		= book.Status
-	m.Editor 		= book.Editor
+	m = book.ToBookResult()
 
+	m.CreateName 		= member.Account
 	m.MemberId 		= relationship.MemberId
 	m.RoleId		= relationship.RoleId
 	m.RelationshipId	= relationship.RelationshipId

+ 10 - 0
models/document.go

@@ -112,7 +112,17 @@ func (m *Document) RecursiveDocument(doc_id int) error {
 	return nil
 }
 
+func (m *Document) ReleaseContent(book_id int)  {
 
+	o := orm.NewOrm()
+
+	_,err := o.Raw("UPDATE md_documents SET `release` = content WHERE book_id =?",book_id).Exec()
+
+	if err != nil {
+		beego.Error(err)
+	}
+
+}
 
 
 

+ 107 - 1
models/document_tree.go

@@ -2,6 +2,10 @@ package models
 
 import (
 	"github.com/astaxie/beego/orm"
+	"bytes"
+	"strconv"
+	"github.com/astaxie/beego"
+	"html/template"
 )
 
 type DocumentTree struct {
@@ -9,6 +13,7 @@ type DocumentTree struct {
 	DocumentName string 		`json:"text"`
 	ParentId interface{}            `json:"parent"`
 	Identify string 		`json:"identify"`
+	BookIdentify string 		`json:"-"`
 	Version int64			`json:"version"`
 	State *DocumentSelected         `json:"state,omitempty"`
 }
@@ -17,7 +22,7 @@ type DocumentSelected struct {
 	Opened bool        	`json:"opened"`
 }
 
-
+//获取项目的文档树状结构
 func (m *Document) FindDocumentTree(book_id int) ([]*DocumentTree,error){
 	o := orm.NewOrm()
 
@@ -30,6 +35,7 @@ func (m *Document) FindDocumentTree(book_id int) ([]*DocumentTree,error){
 	if err != nil {
 		return trees,err
 	}
+	book,_ := NewBook().Find(book_id)
 
 	trees = make([]*DocumentTree,count)
 
@@ -41,6 +47,7 @@ func (m *Document) FindDocumentTree(book_id int) ([]*DocumentTree,error){
 		tree.DocumentId = item.DocumentId
 		tree.Identify = item.Identify
 		tree.Version = item.Version
+		tree.BookIdentify = book.Identify
 		if item.ParentId > 0 {
 			tree.ParentId = item.ParentId
 		}else{
@@ -54,3 +61,102 @@ func (m *Document) FindDocumentTree(book_id int) ([]*DocumentTree,error){
 
 	return trees,nil
 }
+
+func (m *Document) CreateDocumentTreeForHtml(book_id, selected_id int) (string,error) {
+	trees,err := m.FindDocumentTree(book_id)
+	if err != nil {
+		return "",err
+	}
+	parent_id := getSelectedNode(trees,selected_id)
+
+	buf := bytes.NewBufferString("")
+
+	getDocumentTree(trees,0,selected_id,parent_id,buf)
+
+	return buf.String(),nil
+	
+}
+
+//使用递归的方式获取指定ID的顶级ID
+func getSelectedNode(array []*DocumentTree, parent_id int) int {
+
+	for _,item := range array {
+		if _,ok := item.ParentId.(string); ok && item.DocumentId == parent_id  {
+			return item.DocumentId
+		}else if pid,ok := item.ParentId.(int); ok  && item.DocumentId == parent_id{
+			return getSelectedNode(array,pid)
+		}
+	}
+	return  0
+}
+
+func getDocumentTree(array []*DocumentTree,parent_id int,selected_id int,selected_parent_id int,buf *bytes.Buffer)   {
+	buf.WriteString("<ul>")
+
+	for _,item := range array {
+		pid := 0
+
+		if p,ok := item.ParentId.(int);ok {
+			pid = p
+		}
+		if pid == parent_id {
+			/**
+			$selected = $item['doc_id'] == $selected_id ? ' class="jstree-clicked"' : '';
+                $selected_li = $item['doc_id'] == $selected_parent_id ? ' class="jstree-open"' : '';
+
+                $menu .= '<li id="'.$item['doc_id'].'"'.$selected_li.'><a href="'. route('document.show',['doc_id'=> $item['doc_id']]) .'" title="' . htmlspecialchars($item['doc_name']) . '"'.$selected.'>' . $item['doc_name'] .'</a>';
+
+                $key = array_search($item['doc_id'], array_column($array, 'parent_id'));
+
+                if ($key !== false) {
+                    self::createTree($item['doc_id'], $array,$selected_id,$selected_parent_id);
+                }
+                $menu .= '</li>';
+			 */
+			selected := ""
+			if item.DocumentId == selected_id {
+				selected = ` class="jstree-clicked"`
+			}
+			selected_li := ""
+			if item.DocumentId == selected_parent_id {
+				selected_li = ` class="jstree-open"`
+			}
+			buf.WriteString("<li id=\"")
+			buf.WriteString(strconv.Itoa(item.DocumentId))
+			buf.WriteString("\"")
+			buf.WriteString(selected_li)
+			buf.WriteString("><a href=\"")
+			if item.Identify != ""{
+				uri := beego.URLFor("DocumentController.Read",":key",item.BookIdentify,":id" ,item.Identify)
+				buf.WriteString(uri)
+			}else{
+				uri := beego.URLFor("DocumentController.Read",":key",item.BookIdentify,":id" ,item.DocumentId)
+				buf.WriteString(uri)
+			}
+			buf.WriteString("\" title=\"")
+			buf.WriteString(template.HTMLEscapeString(item.DocumentName) + "\"")
+			buf.WriteString(selected + ">")
+			buf.WriteString(template.HTMLEscapeString(item.DocumentName) + "</a>")
+
+			for _,sub := range array {
+				if p,ok := sub.ParentId.(int);ok && p == item.DocumentId{
+					getDocumentTree(array,p,selected_id,selected_parent_id,buf)
+				}
+			}
+			buf.WriteString("</li>")
+
+		}
+	}
+	buf.WriteString("</ul>")
+}
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 21
models/member.go

@@ -60,6 +60,7 @@ func (m *Member) Login(account string,password string) (*Member,error) {
 	ok,err := utils.PasswordVerify(member.Password,password) ;
 
 	if ok && err == nil {
+		m.ResolveRoleName()
 		return member,nil
 	}
 
@@ -67,22 +68,23 @@ func (m *Member) Login(account string,password string) (*Member,error) {
 }
 
 // Add 添加一个用户.
-func (member *Member) Add () (error) {
+func (m *Member) Add () (error) {
 	o := orm.NewOrm()
 
-	hash ,err := utils.PasswordHash(member.Password);
+	hash ,err := utils.PasswordHash(m.Password);
 
 	if  err != nil {
 		return err
 	}
 
-	member.Password = hash
+	m.Password = hash
 
-	_,err = o.Insert(member)
+	_,err = o.Insert(m)
 
 	if err != nil {
 		return err
 	}
+	m.ResolveRoleName()
 	return  nil
 }
 
@@ -103,22 +105,16 @@ func (m *Member) Find(id int) error{
 	if err := o.Read(m); err != nil {
 		return  err
 	}
-	if m.Role == conf.MemberSuperRole {
-		m.RoleName = "超级管理员"
-	}else if m.Role == conf.MemberAdminRole {
-		m.RoleName = "管理员"
-	}else if m.Role == conf.MemberGeneralRole {
-		m.RoleName = "普通用户"
-	}
+	m.ResolveRoleName()
 	return nil
 }
 
 func (m *Member) ResolveRoleName (){
-	if m.Role == 0 {
+	if m.Role == conf.MemberSuperRole {
 		m.RoleName = "超级管理员"
-	}else if m.Role == 1 {
+	}else if m.Role == conf.MemberAdminRole {
 		m.RoleName = "管理员"
-	}else if m.Role == 2 {
+	}else if m.Role == conf.MemberGeneralRole {
 		m.RoleName = "普通用户"
 	}
 }
@@ -154,13 +150,7 @@ func (m *Member) FindToPager(pageIndex, pageSize int) ([]*Member,int64,error)  {
 	}
 
 	for _,m := range members {
-		if m.Role == 0 {
-			m.RoleName = "超级管理员"
-		}else if m.Role == 1 {
-			m.RoleName = "管理员"
-		}else if m.Role == 2 {
-			m.RoleName = "普通用户"
-		}
+		m.ResolveRoleName()
 	}
 	return members,totalCount,nil
 }

+ 2 - 1
models/options.go

@@ -67,9 +67,10 @@ func GetOptionValue(key, def string) string {
 func (p *Option) InsertOrUpdate() error  {
 
 	o := orm.NewOrm()
+
 	var err error
 	if p.OptionId > 0 {
-		_,err = o.Update(o)
+		_,err = o.Update(p)
 	}else{
 		_,err = o.Insert(p)
 	}

+ 25 - 0
routers/filter.go

@@ -0,0 +1,25 @@
+package routers
+
+import (
+	"github.com/astaxie/beego"
+	"github.com/astaxie/beego/context"
+	"github.com/lifei6671/godoc/conf"
+	"github.com/lifei6671/godoc/models"
+)
+
+func init()  {
+	var FilterUser = func(ctx *context.Context) {
+		_, ok := ctx.Input.Session(conf.LoginSessionName).(models.Member)
+
+		if !ok {
+			ctx.Redirect(302, beego.URLFor("AccountController.Login"))
+		}
+	}
+	beego.InsertFilter("/manager",beego.BeforeRouter,FilterUser)
+	beego.InsertFilter("/manager/*",beego.BeforeRouter,FilterUser)
+	beego.InsertFilter("/setting",beego.BeforeRouter,FilterUser)
+	beego.InsertFilter("/setting/*",beego.BeforeRouter,FilterUser)
+	beego.InsertFilter("/book",beego.BeforeRouter,FilterUser)
+	beego.InsertFilter("/book/*",beego.BeforeRouter,FilterUser)
+	beego.InsertFilter("/api/*",beego.BeforeRouter,FilterUser)
+}

+ 8 - 7
routers/router.go

@@ -12,9 +12,10 @@ func init()  {
 	beego.Router("/logout", &controllers.AccountController{},"*:Logout")
 	beego.Router("/register", &controllers.AccountController{},"*:Register")
 	beego.Router("/find_password", &controllers.AccountController{},"*:FindPassword")
+	beego.Router("/captcha", &controllers.AccountController{},"*:Captcha")
 
 	beego.Router("/manager", &controllers.ManagerController{},"*:Index")
-	beego.Router("/manager/users", &controllers.ManagerController{})
+	beego.Router("/manager/users", &controllers.ManagerController{},"*:Users")
 	beego.Router("/manager/member/create", &controllers.ManagerController{},"post:CreateMember")
 	beego.Router("/manager/member/update-member-status",&controllers.ManagerController{},"post:UpdateMemberStatus")
 	beego.Router("/manager/member/change-member-role", &controllers.ManagerController{},"post:ChangeMemberRole")
@@ -47,15 +48,15 @@ func init()  {
 	beego.Router("/book/setting/token", &controllers.BookController{},"post:CreateToken")
 	beego.Router("/book/setting/delete", &controllers.BookController{},"post:Delete")
 
-	beego.Router("/docs/:key/edit/?:id", &controllers.DocumentController{},"*:Edit")
-	beego.Router("/docs/upload",&controllers.DocumentController{},"post:Upload")
-	beego.Router("/docs/:key/create",&controllers.DocumentController{},"post:Create")
-	beego.Router("/docs/:key/delete", &controllers.DocumentController{},"post:Delete")
-	beego.Router("/docs/:key/content/?:id",&controllers.DocumentController{},"*:Content")
+	beego.Router("/api/:key/edit/?:id", &controllers.DocumentController{},"*:Edit")
+	beego.Router("/api/upload",&controllers.DocumentController{},"post:Upload")
+	beego.Router("/api/:key/create",&controllers.DocumentController{},"post:Create")
+	beego.Router("/api/:key/delete", &controllers.DocumentController{},"post:Delete")
+	beego.Router("/api/:key/content/?:id",&controllers.DocumentController{},"*:Content")
 
 
 	beego.Router("/docs/:key", &controllers.DocumentController{},"*:Index")
 	beego.Router("/docs/:key/:id", &controllers.DocumentController{},"*:Read")
 
-	beego.Router("/:key/attach_files/:attach_id",&controllers.DocumentController{},"get:DownloadAttachment")
+	beego.Router("/attach_files/:key/:attach_id",&controllers.DocumentController{},"get:DownloadAttachment")
 }

+ 40 - 0
start.sh

@@ -0,0 +1,40 @@
+#!/bin/sh
+set -e
+
+cd /go/src/github.com/lifei6671/godoc/
+
+goFile="godoc"
+
+
+chmod +x $goFile
+
+if [ ! -f "conf/app.conf" ] ; then
+    cp conf/app.conf.example conf/app.conf
+fi
+
+if [ ! -z $db_host ] ; then
+    sed -i 's/^db_host.*/db_host='$db_host'/g' conf/app.conf
+fi
+
+if [ ! -z $db_port ] ; then
+    sed -i 's/^db_port.*/db_port='$db_port'/g' conf/app.conf
+fi
+
+if [ ! -z $db_database ] ; then
+    sed -i 's/^db_database.*/db_database='$db_database'/g' conf/app.conf
+fi
+
+if [ ! -z $db_username ] ; then
+    sed -i 's/^db_username.*/db_username='$db_username'/g' conf/app.conf
+fi
+
+if [ ! -z $db_password ] ; then
+    sed -i 's/^db_password.*/db_password='$db_password'/g' conf/app.conf
+fi
+
+if [ ! -z $httpport ] ; then
+    sed -i 's/^httpport.*/httpport='$httpport'/g' conf/app.conf
+fi
+
+
+./$goFile

+ 72 - 2
static/css/kancloud.css

@@ -141,6 +141,71 @@ h6 {
     zoom:1;border-bottom: 1px solid #ddd
 }
 
+.m-manual .manual-tab .tab-util {
+    position: absolute;
+    top: 50%;
+    right: -14px
+}
+.m-manual .manual-tab .tab-util .item {
+    color: #999;
+    cursor: pointer;
+    height: 24px;
+    line-height: 24px;
+    display: inline-block;
+    margin-top: 4px
+}
+
+.manual-fullscreen-switch {
+    display: block
+}
+
+.manual-fullscreen-switch .open,.manual-fullscreen-switch .close {
+    display: inline-block;
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+    background-color: #5cb85c;
+    border-radius: 50%;
+    color: #fff;
+    position: relative;
+    font-size: 16px;
+    vertical-align: top;
+    opacity : 1;
+    text-shadow:none;
+    font-weight: 400;
+}
+.manual-fullscreen-switch .open:hover,.manual-fullscreen-switch .close:hover {
+    background-color: #449d44;
+}
+
+.manual-fullscreen-switch .open:before,.manual-fullscreen-switch .close:before {
+    position: absolute;
+    top: 7px;
+    right: 5px;
+}
+
+.manual-fullscreen-switch .open {
+    display: none;
+}
+
+.m-manual.manual-fullscreen-active .manual-fullscreen-switch {
+    /*margin-top: 30px;*/
+}
+
+.m-manual.manual-fullscreen-active .manual-fullscreen-switch .open {
+    display: inline-block;
+}
+
+.m-manual.manual-fullscreen-active .manual-fullscreen-switch .close {
+    display: none;
+}
+.m-manual.manual-fullscreen-active .manual-left .m-copyright,.m-manual.manual-fullscreen-active .manual-left .tab-navg,.m-manual.manual-fullscreen-active .manual-left .tab-wrap{
+    display: none;
+}
+.m-manual.manual-fullscreen-active .manual-left{
+    width: 0px;
+}
+
 .m-manual .manual-tab .tab-navg:after {
     content: '.';
     display: block;
@@ -149,7 +214,7 @@ h6 {
     line-height: 9;
     overflow: hidden;
     clear: both;
-    visibility: hidden
+    visibility: hidden;
 }
 
 .m-manual .manual-tab .tab-navg .navg-item {
@@ -246,6 +311,9 @@ h6 {
     -o-transition-timing-function: linear;
     -o-transition-delay: 0s
 }
+.m-manual.manual-fullscreen-active .manual-right{
+    left: 0;
+}
 .m-manual .manual-right .manual-article{
     background: #ffffff;
 }
@@ -266,7 +334,9 @@ h6 {
     color: #7e888b
 }
 .manual-article .article-content{
-    max-width: 980px;
+    min-width: 980px;
+    max-width: 98%;
+    padding: 10px 20px;
     margin-left: auto!important;
     margin-right: auto!important
 }

+ 1 - 1
static/css/main.css

@@ -193,7 +193,7 @@ textarea{
     position: relative;
 }
 .manual-list .list-item .manual-item-standard .cover {
-    border: 1px solid #f3f3f3;
+    border: 1px solid #999999;
     width: 171px;
     position: relative;
     display: inline-block;

+ 0 - 0
static/js/edirot.js → static/js/editor.js


+ 1 - 0
static/js/markdown.js

@@ -84,6 +84,7 @@ $(function () {
             if(Object.prototype.toString.call(window.documentCategory) === '[object Array]' && window.documentCategory.length > 0){
                 $.ajax({
                     url : window.releaseURL,
+                    data :{"identify" : window.book.identify },
                     type : "post",
                     dataType : "json",
                     success : function (res) {

+ 74 - 0
static/nprogress/nprogress.css

@@ -0,0 +1,74 @@
+/* Make clicks pass-through */
+#nprogress {
+  pointer-events: none;
+}
+
+#nprogress .bar {
+  background: #29d;
+
+  position: fixed;
+  z-index: 1031;
+  top: 0;
+  left: 0;
+
+  width: 100%;
+  height: 2px;
+}
+
+/* Fancy blur effect */
+#nprogress .peg {
+  display: block;
+  position: absolute;
+  right: 0px;
+  width: 100px;
+  height: 100%;
+  box-shadow: 0 0 10px #29d, 0 0 5px #29d;
+  opacity: 1.0;
+
+  -webkit-transform: rotate(3deg) translate(0px, -4px);
+      -ms-transform: rotate(3deg) translate(0px, -4px);
+          transform: rotate(3deg) translate(0px, -4px);
+}
+
+/* Remove these to get rid of the spinner */
+#nprogress .spinner {
+  display: block;
+  position: fixed;
+  z-index: 1031;
+  top: 15px;
+  right: 15px;
+}
+
+#nprogress .spinner-icon {
+  width: 18px;
+  height: 18px;
+  box-sizing: border-box;
+
+  border: solid 2px transparent;
+  border-top-color: #29d;
+  border-left-color: #29d;
+  border-radius: 50%;
+
+  -webkit-animation: nprogress-spinner 400ms linear infinite;
+          animation: nprogress-spinner 400ms linear infinite;
+}
+
+.nprogress-custom-parent {
+  overflow: hidden;
+  position: relative;
+}
+
+.nprogress-custom-parent #nprogress .spinner,
+.nprogress-custom-parent #nprogress .bar {
+  position: absolute;
+}
+
+@-webkit-keyframes nprogress-spinner {
+  0%   { -webkit-transform: rotate(0deg); }
+  100% { -webkit-transform: rotate(360deg); }
+}
+@keyframes nprogress-spinner {
+  0%   { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+

+ 476 - 0
static/nprogress/nprogress.js

@@ -0,0 +1,476 @@
+/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
+ * @license MIT */
+
+;(function(root, factory) {
+
+  if (typeof define === 'function' && define.amd) {
+    define(factory);
+  } else if (typeof exports === 'object') {
+    module.exports = factory();
+  } else {
+    root.NProgress = factory();
+  }
+
+})(this, function() {
+  var NProgress = {};
+
+  NProgress.version = '0.2.0';
+
+  var Settings = NProgress.settings = {
+    minimum: 0.08,
+    easing: 'ease',
+    positionUsing: '',
+    speed: 200,
+    trickle: true,
+    trickleRate: 0.02,
+    trickleSpeed: 800,
+    showSpinner: true,
+    barSelector: '[role="bar"]',
+    spinnerSelector: '[role="spinner"]',
+    parent: 'body',
+    template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
+  };
+
+  /**
+   * Updates configuration.
+   *
+   *     NProgress.configure({
+   *       minimum: 0.1
+   *     });
+   */
+  NProgress.configure = function(options) {
+    var key, value;
+    for (key in options) {
+      value = options[key];
+      if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
+    }
+
+    return this;
+  };
+
+  /**
+   * Last number.
+   */
+
+  NProgress.status = null;
+
+  /**
+   * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
+   *
+   *     NProgress.set(0.4);
+   *     NProgress.set(1.0);
+   */
+
+  NProgress.set = function(n) {
+    var started = NProgress.isStarted();
+
+    n = clamp(n, Settings.minimum, 1);
+    NProgress.status = (n === 1 ? null : n);
+
+    var progress = NProgress.render(!started),
+        bar      = progress.querySelector(Settings.barSelector),
+        speed    = Settings.speed,
+        ease     = Settings.easing;
+
+    progress.offsetWidth; /* Repaint */
+
+    queue(function(next) {
+      // Set positionUsing if it hasn't already been set
+      if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
+
+      // Add transition
+      css(bar, barPositionCSS(n, speed, ease));
+
+      if (n === 1) {
+        // Fade out
+        css(progress, { 
+          transition: 'none', 
+          opacity: 1 
+        });
+        progress.offsetWidth; /* Repaint */
+
+        setTimeout(function() {
+          css(progress, { 
+            transition: 'all ' + speed + 'ms linear', 
+            opacity: 0 
+          });
+          setTimeout(function() {
+            NProgress.remove();
+            next();
+          }, speed);
+        }, speed);
+      } else {
+        setTimeout(next, speed);
+      }
+    });
+
+    return this;
+  };
+
+  NProgress.isStarted = function() {
+    return typeof NProgress.status === 'number';
+  };
+
+  /**
+   * Shows the progress bar.
+   * This is the same as setting the status to 0%, except that it doesn't go backwards.
+   *
+   *     NProgress.start();
+   *
+   */
+  NProgress.start = function() {
+    if (!NProgress.status) NProgress.set(0);
+
+    var work = function() {
+      setTimeout(function() {
+        if (!NProgress.status) return;
+        NProgress.trickle();
+        work();
+      }, Settings.trickleSpeed);
+    };
+
+    if (Settings.trickle) work();
+
+    return this;
+  };
+
+  /**
+   * Hides the progress bar.
+   * This is the *sort of* the same as setting the status to 100%, with the
+   * difference being `done()` makes some placebo effect of some realistic motion.
+   *
+   *     NProgress.done();
+   *
+   * If `true` is passed, it will show the progress bar even if its hidden.
+   *
+   *     NProgress.done(true);
+   */
+
+  NProgress.done = function(force) {
+    if (!force && !NProgress.status) return this;
+
+    return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
+  };
+
+  /**
+   * Increments by a random amount.
+   */
+
+  NProgress.inc = function(amount) {
+    var n = NProgress.status;
+
+    if (!n) {
+      return NProgress.start();
+    } else {
+      if (typeof amount !== 'number') {
+        amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95);
+      }
+
+      n = clamp(n + amount, 0, 0.994);
+      return NProgress.set(n);
+    }
+  };
+
+  NProgress.trickle = function() {
+    return NProgress.inc(Math.random() * Settings.trickleRate);
+  };
+
+  /**
+   * Waits for all supplied jQuery promises and
+   * increases the progress as the promises resolve.
+   *
+   * @param $promise jQUery Promise
+   */
+  (function() {
+    var initial = 0, current = 0;
+
+    NProgress.promise = function($promise) {
+      if (!$promise || $promise.state() === "resolved") {
+        return this;
+      }
+
+      if (current === 0) {
+        NProgress.start();
+      }
+
+      initial++;
+      current++;
+
+      $promise.always(function() {
+        current--;
+        if (current === 0) {
+            initial = 0;
+            NProgress.done();
+        } else {
+            NProgress.set((initial - current) / initial);
+        }
+      });
+
+      return this;
+    };
+
+  })();
+
+  /**
+   * (Internal) renders the progress bar markup based on the `template`
+   * setting.
+   */
+
+  NProgress.render = function(fromStart) {
+    if (NProgress.isRendered()) return document.getElementById('nprogress');
+
+    addClass(document.documentElement, 'nprogress-busy');
+    
+    var progress = document.createElement('div');
+    progress.id = 'nprogress';
+    progress.innerHTML = Settings.template;
+
+    var bar      = progress.querySelector(Settings.barSelector),
+        perc     = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
+        parent   = document.querySelector(Settings.parent),
+        spinner;
+    
+    css(bar, {
+      transition: 'all 0 linear',
+      transform: 'translate3d(' + perc + '%,0,0)'
+    });
+
+    if (!Settings.showSpinner) {
+      spinner = progress.querySelector(Settings.spinnerSelector);
+      spinner && removeElement(spinner);
+    }
+
+    if (parent != document.body) {
+      addClass(parent, 'nprogress-custom-parent');
+    }
+
+    parent.appendChild(progress);
+    return progress;
+  };
+
+  /**
+   * Removes the element. Opposite of render().
+   */
+
+  NProgress.remove = function() {
+    removeClass(document.documentElement, 'nprogress-busy');
+    removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
+    var progress = document.getElementById('nprogress');
+    progress && removeElement(progress);
+  };
+
+  /**
+   * Checks if the progress bar is rendered.
+   */
+
+  NProgress.isRendered = function() {
+    return !!document.getElementById('nprogress');
+  };
+
+  /**
+   * Determine which positioning CSS rule to use.
+   */
+
+  NProgress.getPositioningCSS = function() {
+    // Sniff on document.body.style
+    var bodyStyle = document.body.style;
+
+    // Sniff prefixes
+    var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
+                       ('MozTransform' in bodyStyle) ? 'Moz' :
+                       ('msTransform' in bodyStyle) ? 'ms' :
+                       ('OTransform' in bodyStyle) ? 'O' : '';
+
+    if (vendorPrefix + 'Perspective' in bodyStyle) {
+      // Modern browsers with 3D support, e.g. Webkit, IE10
+      return 'translate3d';
+    } else if (vendorPrefix + 'Transform' in bodyStyle) {
+      // Browsers without 3D support, e.g. IE9
+      return 'translate';
+    } else {
+      // Browsers without translate() support, e.g. IE7-8
+      return 'margin';
+    }
+  };
+
+  /**
+   * Helpers
+   */
+
+  function clamp(n, min, max) {
+    if (n < min) return min;
+    if (n > max) return max;
+    return n;
+  }
+
+  /**
+   * (Internal) converts a percentage (`0..1`) to a bar translateX
+   * percentage (`-100%..0%`).
+   */
+
+  function toBarPerc(n) {
+    return (-1 + n) * 100;
+  }
+
+
+  /**
+   * (Internal) returns the correct CSS for changing the bar's
+   * position given an n percentage, and speed and ease from Settings
+   */
+
+  function barPositionCSS(n, speed, ease) {
+    var barCSS;
+
+    if (Settings.positionUsing === 'translate3d') {
+      barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
+    } else if (Settings.positionUsing === 'translate') {
+      barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
+    } else {
+      barCSS = { 'margin-left': toBarPerc(n)+'%' };
+    }
+
+    barCSS.transition = 'all '+speed+'ms '+ease;
+
+    return barCSS;
+  }
+
+  /**
+   * (Internal) Queues a function to be executed.
+   */
+
+  var queue = (function() {
+    var pending = [];
+    
+    function next() {
+      var fn = pending.shift();
+      if (fn) {
+        fn(next);
+      }
+    }
+
+    return function(fn) {
+      pending.push(fn);
+      if (pending.length == 1) next();
+    };
+  })();
+
+  /**
+   * (Internal) Applies css properties to an element, similar to the jQuery 
+   * css method.
+   *
+   * While this helper does assist with vendor prefixed property names, it 
+   * does not perform any manipulation of values prior to setting styles.
+   */
+
+  var css = (function() {
+    var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
+        cssProps    = {};
+
+    function camelCase(string) {
+      return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
+        return letter.toUpperCase();
+      });
+    }
+
+    function getVendorProp(name) {
+      var style = document.body.style;
+      if (name in style) return name;
+
+      var i = cssPrefixes.length,
+          capName = name.charAt(0).toUpperCase() + name.slice(1),
+          vendorName;
+      while (i--) {
+        vendorName = cssPrefixes[i] + capName;
+        if (vendorName in style) return vendorName;
+      }
+
+      return name;
+    }
+
+    function getStyleProp(name) {
+      name = camelCase(name);
+      return cssProps[name] || (cssProps[name] = getVendorProp(name));
+    }
+
+    function applyCss(element, prop, value) {
+      prop = getStyleProp(prop);
+      element.style[prop] = value;
+    }
+
+    return function(element, properties) {
+      var args = arguments,
+          prop, 
+          value;
+
+      if (args.length == 2) {
+        for (prop in properties) {
+          value = properties[prop];
+          if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
+        }
+      } else {
+        applyCss(element, args[1], args[2]);
+      }
+    }
+  })();
+
+  /**
+   * (Internal) Determines if an element or space separated list of class names contains a class name.
+   */
+
+  function hasClass(element, name) {
+    var list = typeof element == 'string' ? element : classList(element);
+    return list.indexOf(' ' + name + ' ') >= 0;
+  }
+
+  /**
+   * (Internal) Adds a class to an element.
+   */
+
+  function addClass(element, name) {
+    var oldList = classList(element),
+        newList = oldList + name;
+
+    if (hasClass(oldList, name)) return; 
+
+    // Trim the opening space.
+    element.className = newList.substring(1);
+  }
+
+  /**
+   * (Internal) Removes a class from an element.
+   */
+
+  function removeClass(element, name) {
+    var oldList = classList(element),
+        newList;
+
+    if (!hasClass(element, name)) return;
+
+    // Replace the class name.
+    newList = oldList.replace(' ' + name + ' ', ' ');
+
+    // Trim the opening and closing spaces.
+    element.className = newList.substring(1, newList.length - 1);
+  }
+
+  /**
+   * (Internal) Gets a space separated list of the class names on the element. 
+   * The list is wrapped with a single space on each end to facilitate finding 
+   * matches within the list.
+   */
+
+  function classList(element) {
+    return (' ' + (element.className || '') + ' ').replace(/\s+/gi, ' ');
+  }
+
+  /**
+   * (Internal) Removes an element from the DOM.
+   */
+
+  function removeElement(element) {
+    element && element.parentNode && element.parentNode.removeChild(element);
+  }
+
+  return NProgress;
+});
+

+ 22 - 2
views/book/dashboard.tpl

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="zh-CN">
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html">
 <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -51,7 +51,7 @@
                         <a href="{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" class="btn btn-default btn-sm pull-right" style="margin-right: 5px;" target="_blank"><i class="fa fa-eye"></i> 阅读</a>
 
                         {{if eq .Model.RoleId 0 1 2}}
-                        <a href="{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" class="btn btn-default btn-sm pull-right" style="margin-right: 5px;" target="_blank"><i class="fa fa-upload" aria-hidden="true"></i> 发布</a>
+                        <button class="btn btn-default btn-sm pull-right" style="margin-right: 5px;" id="btnRelease"><i class="fa fa-upload" aria-hidden="true"></i> 发布</button>
                         {{end}}
                     </div>
                 </div>
@@ -98,7 +98,27 @@
 </div>
 <script src="/static/jquery/1.12.4/jquery.min.js"></script>
 <script src="/static/bootstrap/js/bootstrap.min.js"></script>
+<script src="/static/layer/layer.js"></script>
 <script src="/static/js/main.js" type="text/javascript"></script>
+<script type="text/javascript">
+    $(function () {
+        $("#btnRelease").on("click",function () {
+            $.ajax({
+                url : "{{urlfor "BookController.Release" ":key" .Model.Identify}}",
+                data :{"identify" : "{{.Model.Identify}}" },
+                type : "post",
+                dataType : "json",
+                success : function (res) {
+                    if(res.errcode === 0){
+                        layer.msg("发布任务已推送到任务队列,稍后将在后台执行。");
+                    }else{
+                        layer.msg(res.message);
+                    }
+                }
+            });
+        });
 
+    });
+</script>
 </body>
 </html>

+ 1 - 1
views/book/index.tpl

@@ -60,7 +60,7 @@
                                 <div class="pull-right">
                                     <a :href="'{{urlfor "DocumentController.Index" ":key" ""}}' + item.identify" title="查看文档" data-toggle="tooltip"><i class="fa fa-eye"></i> 查看文档</a>
                                     <template v-if="item.role_id != 3">
-                                        <a :href="'/docs/' + item.identify + '/edit'" title="编辑文档" data-toggle="tooltip"><i class="fa fa-edit" aria-hidden="true"></i> 编辑文档</a>
+                                        <a :href="'/api/' + item.identify + '/edit'" title="编辑文档" data-toggle="tooltip"><i class="fa fa-edit" aria-hidden="true"></i> 编辑文档</a>
                                     </template>
                                 </div>
                                 <div class="clearfix"></div>

+ 265 - 0
views/document/default_read.tpl

@@ -0,0 +1,265 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <title>编辑文档 - Powered by MinDoc</title>
+
+    <!-- Bootstrap -->
+    <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+    <link href="/static/font-awesome/css/font-awesome.min.css" rel="stylesheet">
+    <link href="/static/jstree/3.3.4/themes/default/style.min.css" rel="stylesheet">
+
+    <link href="/static/nprogress/nprogress.css" rel="stylesheet">
+    <link href="/static/css/kancloud.css" rel="stylesheet">
+    <link href="/static/css/jstree.css" rel="stylesheet">
+    {{if eq .Model.Editor "markdown"}}
+    <link href="/static/editor.md/css/editormd.preview.css" rel="stylesheet">
+    {{else}}
+    <link href="/static/highlight/styles/zenburn.css" rel="stylesheet">
+    {{end}}
+    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
+    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+    <!--[if lt IE 9]>
+    <script src="/static/html5shiv/3.7.3/html5shiv.min.js"></script>
+    <script src="/static/respond.js/1.4.2/respond.min.js"></script>
+    <![endif]-->
+</head>
+<body>
+<div class="m-manual manual-reader">
+    <header class="navbar navbar-static-top manual-head" role="banner">
+        <div class="container-fluid">
+            <div class="navbar-header pull-left manual-title">
+                <span class="slidebar" id="slidebar"><i class="fa fa-align-justify"></i></span>
+                {{.Model.BookName}}
+                <span style="font-size: 12px;font-weight: 100;"></span>
+            </div>
+            <div class="navbar-header pull-right manual-menu">
+                <div class="dropdown">
+                    <button id="dLabel" class="btn btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                        项目
+                        <span class="caret"></span>
+                    </button>
+                    <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="dLabel">
+                        {{if gt .Member.MemberId 0}}
+                        {{if eq .Model.RoleId 0 1 2}}
+                        <li><a href="{{urlfor "DocumentController.Edit" ":key" .Model.Identify ":id" ""}}">返回编辑</a> </li>
+                        {{end}}
+                        <li><a href="{{urlfor "BookController.Index"}}">我的项目</a> </li>
+                        <li role="presentation" class="divider"></li>
+                        {{end}}
+                        {{if eq .Model.PrivatelyOwned 0}}
+                        <li><a href="javascript:" data-toggle="modal" data-target="#shareProject">项目分享</a> </li>
+                        <li role="presentation" class="divider"></li>
+                        {{/*<li><a href="https://wiki.iminho.me/export/1" target="_blank">项目导出</a> </li>*/}}
+                        {{end}}
+
+                        <li><a href="{{urlfor "HomeController.Index"}}" title="返回首页">返回首页</a> </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </header>
+    <article class="container-fluid manual-body">
+        <div class="manual-left">
+            <div class="manual-tab">
+                <div class="tab-navg">
+                    <span data-mode="view" class="navg-item active"><i class="fa fa-align-justify"></i><b class="text">目录</b></span>
+                </div>
+                <div class="tab-util">
+                    <span class="manual-fullscreen-switch">
+                        <b class="open fa fa-angle-right" title="展开"></b>
+                        <b class="close fa fa-angle-left" title="关闭"></b>
+                    </span>
+                </div>
+                <div class="tab-wrap">
+                    <div class="tab-item manual-catalog">
+                        <div class="catalog-list read-book-preview" id="sidebar">
+{{.Result}}
+                        </div>
+
+                    </div>
+                </div>
+            </div>
+            <div class="m-copyright">
+                <p>
+                    本文档使用 <a href="https://doc.iminho.me" target="_blank">MinDoc</a> 发布
+                </p>
+            </div>
+        </div>
+        <div class="manual-right">
+            <div class="manual-article">
+                <div class="article-head">
+                    <div class="container-fluid">
+                        <div class="row">
+                            <div class="col-md-2">
+
+                            </div>
+                            <div class="col-md-8 text-center">
+                                <h1 id="article-title">{{.Title}}</h1>
+                            </div>
+                            <div class="col-md-2">
+                            </div>
+                        </div>
+                    </div>
+
+                </div>
+                <div class="article-content">
+                    <div class="article-body  {{if eq .Model.Editor "markdown"}}markdown-body editormd-preview-container{{else}}editor-content{{end}}"  id="page-content">
+                        {{.Content}}
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="manual-progress"><b class="progress-bar"></b></div>
+    </article>
+    <div class="manual-mask"></div>
+</div>
+
+<!-- Share Modal -->
+<div class="modal fade" id="shareProject" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+                <h4 class="modal-title" id="myModalLabel">项目分享</h4>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="password" class="col-sm-2 control-label">项目地址</label>
+                    <div class="col-sm-10">
+                        <input type="text" value="{{.BaseUrl}}{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" class="form-control" onmouseover="this.select()" id="projectUrl" title="项目地址">
+                    </div>
+                    <div class="clearfix"></div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script src="/static/jquery/1.12.4/jquery.min.js"></script>
+<script src="/static/bootstrap/js/bootstrap.min.js"></script>
+<script src="/static/jstree/3.3.4/jstree.min.js" type="text/javascript"></script>
+<script type="text/javascript" src="/static/nprogress/nprogress.js"></script>
+<script type="text/javascript" src="/static/highlight/highlight.js"></script>
+<script type="text/javascript" src="/static/highlight/highlightjs-line-numbers.min.js"></script>
+<script type="text/javascript">
+    var events = $("body");
+    var catalog = null;
+    function initHighlighting() {
+        $('pre code').each(function (i, block) {
+            hljs.highlightBlock(block);
+        });
+
+        hljs.initLineNumbersOnLoad();
+    }
+
+    $(function () {
+        window.isFullScreen = false;
+
+        initHighlighting();
+
+        $("#sidebar").jstree({
+            'plugins':["wholerow","types"],
+            "types": {
+                "default" : {
+                    "icon" : false  // 删除默认图标
+                }
+            },
+            'core' : {
+                'check_callback' : true,
+                "multiple" : false ,
+                'animation' : 0
+            }
+        }).on('select_node.jstree',function (node,selected,event) {
+            $(".m-manual").removeClass('manual-mobile-show-left');
+            var url = selected.node.a_attr.href;
+
+            if(url === window.location.href){
+                return false;
+            }
+            $.ajax({
+                url : url,
+                type : "GET",
+                beforeSend :function (xhr) {
+                    var body = events.data('body_' + selected.node.id);
+                    var title = events.data('title_' + selected.node.id);
+                    var doc_title = events.data('doc_title_' + selected.node.id);
+
+                    if(body && title && doc_title){
+
+                        $("#page-content").html(body);
+                        $("title").text(title);
+                        $("#article-title").text(doc_title);
+
+                        events.trigger('article.open',url,true);
+
+                        return false;
+                    }
+                    NProgress.start();
+                },
+                success : function (res) {
+                    if(res.errcode === 0){
+                        var body = res.data.body;
+                        var doc_title = res.data.doc_title;
+                        var title = res.data.title;
+
+                        $("#page-content").html(body);
+                        $("title").text(title);
+                        $("#article-title").text(doc_title);
+
+                        events.data('body_' + selected.node.id,body);
+                        events.data('title_' + selected.node.id,title);
+                        events.data('doc_title_' + selected.node.id,doc_title);
+
+                        events.trigger('article.open',url,false);
+
+                    }else{
+                        layer.msg("加载失败");
+                    }
+                },
+                complete : function () {
+                    NProgress.done();
+                }
+            });
+        });
+
+        $("#slidebar").on("click",function () {
+            $(".m-manual").addClass('manual-mobile-show-left');
+        });
+        $(".manual-mask").on("click",function () {
+            $(".m-manual").removeClass('manual-mobile-show-left');
+        });
+
+        $(".manual-fullscreen-switch").on("click",function () {
+            isFullScreen = !isFullScreen;
+            if (isFullScreen) {
+                $(".m-manual").addClass('manual-fullscreen-active');
+            } else {
+                $(".m-manual").removeClass('manual-fullscreen-active');
+            }
+        });
+        events.on('article.open', function (event, url,init) {
+            if ('pushState' in history) {
+
+                if (init == false) {
+                    history.replaceState({ }, '', url);
+                    init = true;
+                } else {
+                    history.pushState({ }, '', url);
+                }
+
+            } else {
+                location.hash = url;
+            }
+            initHighlighting();
+
+        });
+    });
+</script>
+</body>
+</html>

+ 1 - 1
views/document/html_edit_template.tpl

@@ -129,7 +129,7 @@
 <script type="text/javascript" src="/static/layer/layer.js"></script>
 <script src="/static/to-markdown/dist/to-markdown.js" type="text/javascript"></script>
 <script src="/static/js/jquery.form.js" type="text/javascript"></script>
-<script src="/static/js/edirot.js" type="text/javascript"></script>
+<script src="/static/js/editor.js" type="text/javascript"></script>
 <script src="/static/js/html-editor.js" type="text/javascript"></script>
 </body>
 </html>

+ 1 - 1
views/document/markdown_edit_template.tpl

@@ -160,7 +160,7 @@
 <script src="/static/editor.md/editormd.js" type="text/javascript"></script>
 <script type="text/javascript" src="/static/layer/layer.js"></script>
 <script src="/static/js/jquery.form.js" type="text/javascript"></script>
-<script src="/static/js/edirot.js" type="text/javascript"></script>
+<script src="/static/js/editor.js" type="text/javascript"></script>
 <script src="/static/js/markdown.js" type="text/javascript"></script>
 </body>
 </html>

+ 0 - 10
views/home/index.html

@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Title</title>
-</head>
-<body>
-
-</body>
-</html>

+ 6 - 137
views/home/index.tpl

@@ -25,158 +25,27 @@
     <div class="container manual-body">
         <div class="row">
             <div class="manual-list">
+                {{range $index,$item := .Lists}}
                 <div class="list-item">
                     <dl class="manual-item-standard">
                         <dt>
-                            <a href="{{urlfor "DocumentController.Index" ":key" "test"}}">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png" class="cover">
+                            <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" title="{{$item.BookName}}-{{$item.CreateName}}">
+                                <img src="{{$item.Cover}}" class="cover" alt="{{$item.BookName}}-{{$item.CreateName}}">
                             </a>
                         </dt>
                         <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
+                            <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" class="name" title="{{$item.BookName}}-{{$item.CreateName}}">{{$item.BookName}}</a>
                         </dd>
                         <dd>
                             <span class="author">
                                 <b class="text">作者</b>
                                 <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
-                            </span>
-                        </dd>
-                    </dl>
-                </div>
-                <div class="list-item">
-                    <dl class="manual-item-standard">
-                        <dt>
-                            <a href="#">
-                                <img src="/uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png">
-                            </a>
-                        </dt>
-                        <dd>
-                            <a href="#" class="name">Docker生产环境实践指南</a>
-                        </dd>
-                        <dd>
-                            <span class="author">
-                                <b class="text">作者</b>
-                                <b class="text">-</b>
-                                <b class="text">Minho</b>
+                                <b class="text">{{$item.CreateName}}</b>
                             </span>
                         </dd>
                     </dl>
                 </div>
+                {{end}}
                 <div class="clearfix"></div>
             </div>
         </div>

+ 14 - 0
views/manager/setting.tpl

@@ -99,7 +99,21 @@
 <script src="/static/js/main.js" type="text/javascript"></script>
 <script type="text/javascript">
     $(function () {
+        $("#gloablEditForm").ajaxForm({
+            beforeSubmit : function () {
+                var title = $.trim($("#siteName").val());
 
+                if (title === ""){
+                    return showError("网站标题不能为空");
+                }
+            },success : function (res) {
+                if(res.errcode === 0) {
+                    showSuccess("保存成功")
+                }else{
+                    showError(res.message);
+                }
+            }
+        });
     });
 </script>
 </body>

+ 11 - 1
views/widgets/header.tpl

@@ -1,7 +1,13 @@
 <header class="navbar navbar-static-top navbar-fixed-top manual-header" role="banner">
     <div class="container">
         <div class="navbar-header col-sm-12 col-md-6 col-lg-5">
-            <a href="/" class="navbar-brand">MinDoc</a>
+            <a href="/" class="navbar-brand">
+                {{if .SITE_TITLE}}
+                {{.SITE_TITLE}}
+                {{else}}
+                {{.SITE_NAME}}
+                {{end}}
+            </a>
             <div class="btn-group dropdown-menu-right pull-right slidebar visible-xs-inline-block visible-sm-inline-block">
                 <button class="btn btn-default dropdown-toggle hidden-lg" type="button" data-toggle="dropdown"><i class="fa fa-align-justify"></i></button>
                 <ul class="dropdown-menu" role="menu">
@@ -19,6 +25,7 @@
         </div>
         <nav class="navbar-collapse hidden-xs hidden-sm" role="navigation">
             <ul class="nav navbar-nav navbar-right">
+                {{if gt .Member.MemberId 0}}
                 <li>
                     <div class="img user-info" data-toggle="dropdown">
                         <img src="{{.Member.Avatar}}" class="img-circle userbar-avatar">
@@ -45,6 +52,9 @@
                         </li>
                     </ul>
                 </li>
+                {{else}}
+                <li><a href="{{urlfor "AccountController.Login"}}" title="用户登录">登录</a></li>
+                {{end}}
             </ul>
         </nav>
     </div>