Przeglądaj źródła

实现文档编辑

Minho 8 lat temu
rodzic
commit
ca1b1a4b83

+ 4 - 1
conf/app.conf

@@ -22,4 +22,7 @@ db_password=123456
 cover=/static/images/book.jpg
 
 #默认编辑器
-editor=markdown
+editor=markdown
+
+#上传文件的后缀
+upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif

+ 4 - 1
conf/app.conf.example

@@ -27,4 +27,7 @@ cover=/static/images/book.jpg
 avatar=/static/images/headimgurl.jpg
 
 #默认阅读令牌长度
-token_size=12
+token_size=12
+
+#上传文件的后缀
+upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif

+ 36 - 1
conf/enumerate.go

@@ -1,7 +1,10 @@
 // package conf 为配置相关.
 package conf
 
-import "github.com/astaxie/beego"
+import (
+	"github.com/astaxie/beego"
+	"strings"
+)
 
 // 登录用户的Session名
 const LoginSessionName = "LoginSessionName"
@@ -53,4 +56,36 @@ func GetTokenSize() int {
 
 func GetDefaultCover() string {
 	return beego.AppConfig.DefaultString("cover","/static/images/book.jpg")
+}
+
+func GetUploadFileExt()  []string {
+	ext := beego.AppConfig.DefaultString("upload_file_ext","png|jpg|jpeg|gif|txt|doc|docx|pdf")
+	
+	temp := strings.Split(ext,"|")
+	
+	exts := make([]string,len(temp))
+	
+	i := 0
+	for _,item := range temp {
+		if item != "" {
+			exts[i] = item
+			i++
+		}
+	}
+	return exts
+}
+
+func IsAllowUploadFileExt(ext string) bool  {
+
+	if strings.HasPrefix(ext,".") {
+		ext = string(ext[1:])
+	}
+	exts := GetUploadFileExt()
+
+	for _,item := range exts {
+		if strings.EqualFold(item,ext) {
+			return  true
+		}
+	}
+	return false
 }

+ 1 - 1
controllers/book.go

@@ -381,7 +381,7 @@ func (c *BookController) Create() {
 			c.JsonResult(6002,"项目标识不能为空")
 		}
 		if ok,err := regexp.MatchString(`^[a-z]+[a-zA-Z0-9_\-]*$`,identify); !ok || err != nil {
-			c.JsonResult(6003,"文档标识只能包含小写字母、数字,以及“-”和“_”符号,并且只能小写字母开头")
+			c.JsonResult(6003,"项目标识只能包含小写字母、数字,以及“-”和“_”符号,并且只能小写字母开头")
 		}
 		if strings.Count(identify,"") > 50 {
 			c.JsonResult(6004,"文档标识不能超过50字")

+ 247 - 3
controllers/document.go

@@ -1,8 +1,21 @@
 package controllers
 
 import (
+	"os"
+	"time"
+	"regexp"
+	"strconv"
+	"strings"
+	"net/http"
+	"path/filepath"
+	"encoding/json"
+	"html/template"
+
 	"github.com/lifei6671/godoc/models"
 	"github.com/astaxie/beego/logs"
+	"github.com/lifei6671/godoc/conf"
+	"github.com/astaxie/beego"
+	"github.com/astaxie/beego/orm"
 )
 
 type DocumentController struct {
@@ -21,12 +34,16 @@ func (c *DocumentController) Edit()  {
 	c.Prepare()
 
 	identify := c.Ctx.Input.Param(":key")
+	if identify == "" {
+		c.Abort("404")
+	}
 
-	book,err := models.NewBook().FindByFieldFirst("identify",identify)
+	book,err := models.NewBookResult().FindByIdentify(identify,c.Member.MemberId)
 
 	if err != nil {
 		logs.Error("DocumentController.Edit => ",err)
-		c.Abort("500")
+
+		c.Abort("403")
 	}
 	if book.Editor == "markdown" {
 		c.TplName = "document/markdown_edit_template.tpl"
@@ -34,4 +51,231 @@ func (c *DocumentController) Edit()  {
 		c.TplName = "document/html_edit_template.tpl"
 	}
 
-}
+	c.Data["Model"] = book
+
+	r,_ := json.Marshal(book)
+
+	c.Data["ModelResult"] = template.JS(string(r))
+
+	c.Data["Result"] = template.JS("[]")
+
+	trees ,err := models.NewDocument().FindDocumentTree(book.BookId)
+	logs.Info("",trees)
+	if err != nil {
+		logs.Error("FindDocumentTree => ", err)
+	}else{
+		if len(trees) > 0 {
+			if jtree, err := json.Marshal(trees); err == nil {
+				c.Data["Result"] = template.JS(string(jtree))
+			}
+		}else{
+			c.Data["Result"] = template.JS("[]")
+		}
+	}
+
+}
+
+//创建一个文档
+func (c *DocumentController) Create() {
+	identify := c.GetString("identify")
+	doc_identify := c.GetString("doc_identify")
+	doc_name := c.GetString("doc_name")
+	parent_id,_ := c.GetInt("parent_id",0)
+
+	if identify == "" {
+		c.JsonResult(6001,"参数错误")
+	}
+	if doc_name == "" {
+		c.JsonResult(6004,"文档名称不能为空")
+	}
+	if doc_identify != "" {
+		if ok, err := regexp.MatchString(`^[a-z]+[a-zA-Z0-9_\-]*$`, doc_identify); !ok || err != nil {
+
+			c.JsonResult(6003, "文档标识只能包含小写字母、数字,以及“-”和“_”符号,并且只能小写字母开头")
+		}
+		d,_ := models.NewDocument().FindByFieldFirst("identify",doc_identify);
+		if  d.DocumentId > 0{
+			c.JsonResult(6006,"文档标识已被使用")
+		}
+	}
+
+	bookResult,err := models.NewBookResult().FindByIdentify(identify,c.Member.MemberId)
+
+	if err != nil || bookResult.RoleId == conf.BookObserver {
+		logs.Error("FindByIdentify => ",err)
+		c.JsonResult(6002,"项目不存在或权限不足")
+	}
+	if parent_id > 0 {
+		doc,err := models.NewDocument().Find(parent_id)
+		if err != nil || doc.BookId != bookResult.BookId{
+			c.JsonResult(6003,"父分类不存在")
+		}
+	}
+
+	document := models.NewDocument()
+	document.MemberId = c.Member.MemberId
+	document.BookId = bookResult.BookId
+	if doc_identify != ""{
+		document.Identify = doc_identify
+	}
+	document.Version = time.Now().UnixNano()
+	document.DocumentName = doc_name
+	document.ParentId = parent_id
+	logs.Info("%+v",document)
+	if err := document.InsertOrUpdate();err != nil {
+		logs.Error("InsertOrUpdate => ",err)
+		c.JsonResult(6005,"保存失败")
+	}else{
+		logs.Info("",document)
+		c.JsonResult(0,"ok",document)
+	}
+}
+
+//上传附件或图片
+func (c *DocumentController) Upload()  {
+
+	identify := c.GetString("identify")
+
+	if identify == "" {
+		c.JsonResult(6001,"参数错误")
+	}
+
+
+	name := "editormd-file-file"
+
+	file,moreFile,err  := c.GetFile(name)
+	if err == http.ErrMissingFile {
+		name = "editormd-image-file"
+		file,moreFile,err = c.GetFile(name);
+		if err == http.ErrMissingFile {
+			c.JsonResult(6003,"没有发现需要上传的文件")
+		}
+	}
+	if err != nil {
+		c.JsonResult(6002,err.Error())
+	}
+
+	defer file.Close()
+
+	ext := filepath.Ext(moreFile.Filename)
+
+	if ext == "" {
+		c.JsonResult(6003,"无法解析文件的格式")
+	}
+
+	if !conf.IsAllowUploadFileExt(ext) {
+		c.JsonResult(6004,"不允许的文件类型")
+	}
+
+	book,err := models.NewBookResult().FindByIdentify(identify,c.Member.MemberId)
+
+	if err != nil {
+		logs.Error("DocumentController.Edit => ",err)
+		if err == orm.ErrNoRows {
+			c.JsonResult(6006,"权限不足")
+		}
+		c.JsonResult(6001,err.Error())
+	}
+	//如果没有编辑权限
+	if book.RoleId != conf.BookEditor && book.RoleId != conf.BookAdmin && book.RoleId != conf.BookFounder {
+		c.JsonResult(6006,"权限不足")
+	}
+
+	fileName := "attachment_" +  strconv.FormatInt(time.Now().UnixNano(), 16)
+
+	filePath := "uploads/" + time.Now().Format("200601") + "/" + fileName + ext
+
+	err = c.SaveToFile(name,filePath)
+
+	if err != nil {
+		logs.Error("SaveToFile => ",err)
+		c.JsonResult(6005,"保存文件失败")
+	}
+	attachment := models.NewAttachment()
+	attachment.BookId = book.BookId
+	attachment.FileName = moreFile.Filename
+	attachment.CreateAt = c.Member.MemberId
+	attachment.FileExt = ext
+	attachment.FilePath = filePath
+
+	if strings.EqualFold(ext,".jpg") || strings.EqualFold(ext,".jpeg") || strings.EqualFold(ext,"png") || strings.EqualFold(ext,"gif") {
+		attachment.HttpPath = c.BaseUrl() + "/" + filePath
+	}
+
+	err = attachment.Insert();
+
+	if err != nil {
+		os.Remove(filePath)
+		logs.Error("Attachment Insert => ",err)
+		c.JsonResult(6006,"文件保存失败")
+	}
+	if attachment.HttpPath == "" {
+		attachment.HttpPath = c.BaseUrl() + beego.URLFor("DocumentController.DownloadAttachment",":key", identify, ":attach_id", attachment.AttachmentId)
+
+		if err := attachment.Update();err != nil {
+			logs.Error("SaveToFile => ",err)
+			c.JsonResult(6005,"保存文件失败")
+		}
+	}
+	result := map[string]interface{}{
+		"success" : 1,
+		"message" :"ok",
+		"url" : attachment.HttpPath,
+		"alt" : attachment.FileName,
+	}
+
+	c.Data["json"] = result
+	c.ServeJSON(true)
+	c.StopRun()
+}
+
+//DownloadAttachment 下载附件.
+func (c *DocumentController) DownloadAttachment()  {
+	c.Prepare()
+
+	identify := c.Ctx.Input.Param(":key")
+	attach_id,_ := strconv.Atoi(c.Ctx.Input.Param(":attach_id"))
+	token := c.GetString("token")
+
+	member_id := 0
+
+	if c.Member != nil {
+		member_id = c.Member.MemberId
+	}
+	book_id := 0
+
+	//判断用户是否参与了项目
+	bookResult,err := models.NewBookResult().FindByIdentify(identify,member_id)
+
+	if err != nil {
+		//判断项目公开状态
+		book,err := models.NewBook().FindByFieldFirst("identify",identify)
+		if err != nil {
+			c.Abort("404")
+		}
+		//如果项目是私有的,并且token不正确
+		if (book.PrivatelyOwned == 1 && token == "" ) || ( book.PrivatelyOwned == 1 && book.PrivateToken != token ){
+			c.Abort("403")
+		}
+		book_id = book.BookId
+	}else{
+		book_id = bookResult.BookId
+	}
+
+	attachment,err := models.NewAttachment().Find(attach_id)
+
+	if err != nil {
+		logs.Error("DownloadAttachment => ", err)
+		if err == orm.ErrNoRows {
+			c.Abort("404")
+		} else {
+			c.Abort("500")
+		}
+	}
+	if attachment.BookId != book_id {
+		c.Abort("404")
+	}
+	c.Ctx.Output.Download(attachment.FilePath,attachment.FileName)
+
+	c.StopRun()
+}

+ 1 - 0
main.go

@@ -16,5 +16,6 @@ func main() {
 
 	beego.SetStaticPath("uploads","uploads")
 
+
 	beego.Run()
 }

+ 9 - 2
models/attachment.go

@@ -9,9 +9,11 @@ import (
 // Attachment struct .
 type Attachment struct {
 	AttachmentId int	`orm:"column(attachment_id);pk;auto;unique" json:"attachment_id"`
-	DocumentId int		`orm:"column(document_id);type(int)" json:"document_id"`
-	FileName string		`orm:"column(file_name);size(2000)" json:"file_name"`
+	BookId int		`orm:"column(book_id);type(int)" json:"book_id"`
+	FileName string 	`orm:"column(file_name);size(255)" json:"file_name"`
+	FilePath string		`orm:"column(file_path);size(2000)" json:"file_path"`
 	FileSize float64	`orm:"column(file_size);type(float)" json:"file_size"`
+	HttpPath string		`orm:"column(http_path);size(2000)" json:"http_path"`
 	FileExt string 		`orm:"column(file_ext);size(50)" json:"file_ext"`
 	CreateTime time.Time	`orm:"type(datetime);column(create_time);auto_now_add"`
 	CreateAt int		`orm:"column(create_at);type(int)" json:"create_at"`
@@ -41,6 +43,11 @@ func (m *Attachment) Insert() error  {
 
 	return err
 }
+func (m *Attachment) Update() error {
+	o := orm.NewOrm()
+	_,err := o.Update(m)
+	return err
+}
 
 func (m *Attachment) Find(id int) (*Attachment,error) {
 	if id <= 0 {

+ 1 - 1
models/comment.go

@@ -84,7 +84,7 @@ func (m *Comment) Insert() error {
 
 	document := NewDocument()
 	//如果评论的文档不存在
-	if err := document.Find(m.DocumentId); err != nil {
+	if _,err := document.Find(m.DocumentId); err != nil {
 		return err
 	}
 	book ,err := NewBook().Find(document.BookId);

+ 69 - 13
models/document.go

@@ -2,32 +2,43 @@ package models
 
 import (
 	"time"
+
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego/orm"
 )
 
 // Document struct.
 type Document struct {
-	DocumentId int		`orm:"pk;auto;unique;column(document_id)" json:"document_id"`
-	DocumentName string	`orm:"column(document_name);size(500)" json:"document_name"`
+	DocumentId int		`orm:"pk;auto;unique;column(document_id)" json:"doc_id"`
+	DocumentName string	`orm:"column(document_name);size(500)" json:"doc_name"`
 	// Identify 文档唯一标识
-	Identify string		`orm:"column(identify);size(100);unique" json:"identify"`
+	Identify string		`orm:"column(identify);size(100);unique;null;default(null)" json:"identify"`
 	BookId int		`orm:"column(book_id);type(int);index" json:"book_id"`
-	OrderSort int		`orm:"column(order_sort);default(0);type(int)" json:"order_sort"`
+	ParentId int 		`orm:"column(parent_id);type(int);index" json:"parent_id"`
+	OrderSort int		`orm:"column(order_sort);default(0);type(int);index" json:"order_sort"`
 	// Markdown markdown格式文档.
 	Markdown string		`orm:"column(markdown);type(longtext)" json:"markdown"`
 	// Release 发布后的Html格式内容.
 	Release string		`orm:"column(release);type(longtext)" json:"release"`
 	// Content 未发布的 Html 格式内容.
 	Content string		`orm:"column(content);type(longtext)" json:"content"`
-	CreateTime time.Time	`orm:"column(create_time);type(datetime)" json:"create_time"`
-	CreateAt int		`orm:"column(create_at);type(int)" json:"create_at"`
+	CreateTime time.Time	`orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
+	MemberId int		`orm:"column(member_id);type(int)" json:"member_id"`
 	ModifyTime time.Time	`orm:"column(modify_time);type(datetime);auto_now" json:"modify_time"`
-	ModifyAt int		`orm:"column(modify_at);type(int)" json:"modify_at"`
-	Version int64		`orm:"type(bigint);column(version)" json:"version"`
+	ModifyAt int		`orm:"column(modify_at);type(int)" json:"-"`
+	Version int64		`orm:"type(bigint);column(version)" json:"-"`
 }
 
-
+type DocumentTree struct {
+	DocumentId int               `json:"id,string"`
+	DocumentName string 		`json:"text"`
+	ParentId int                	`json:"parent_id,string"`
+	State *DocumentSelected         `json:"state,omitempty"`
+}
+type DocumentSelected struct {
+	Selected bool        	`json:"selected"`
+	Opened bool        	`json:"opened"`
+}
 // TableName 获取对应数据库表名.
 func (m *Document) TableName() string {
 	return "documents"
@@ -45,9 +56,9 @@ func NewDocument() *Document  {
 	return &Document{}
 }
 
-func (m *Document) Find(id int) error {
+func (m *Document) Find(id int) (*Document,error) {
 	if id <= 0 {
-		return ErrInvalidParameter
+		return m,ErrInvalidParameter
 	}
 	o := orm.NewOrm()
 
@@ -55,16 +66,61 @@ func (m *Document) Find(id int) error {
 	err := o.Read(m)
 
 	if err == orm.ErrNoRows{
-		return ErrDataNotExist
+		return m,ErrDataNotExist
 	}
-	return nil
+	return m,nil
 }
 
 
+func (m *Document) FindDocumentTree(book_id int) ([]*DocumentTree,error){
+	o := orm.NewOrm()
+
+	trees := make([]*DocumentTree,0)
+
+	var docs []*Document
+
+	count ,err := o.QueryTable(m).Filter("book_id",book_id).OrderBy("-order_sort","document_id").All(&docs,"document_id","document_name","parent_id")
+
+	if err != nil {
+		return trees,err
+	}
+
+	trees = make([]*DocumentTree,count)
+
+	for index,item := range docs {
+		tree := &DocumentTree{}
+		if index == 0{
+			tree.State = &DocumentSelected{ Selected: true, Opened: true }
+		}
+		tree.DocumentId = item.DocumentId
+		tree.ParentId = item.ParentId
+		tree.DocumentName = item.DocumentName
 
+		trees[index] = tree
+	}
 
+	return trees,nil
+}
 
+func (m *Document) InsertOrUpdate(cols... string) error {
+	o := orm.NewOrm()
 
+	if m.DocumentId > 0 {
+		_,err := o.Update(m)
+		return err
+	}else{
+		_,err := o.Insert(m)
+		return err
+	}
+}
+
+func (m *Document) FindByFieldFirst(field string,v interface{}) (*Document,error) {
+	o := orm.NewOrm()
+
+	err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,v).One(m)
+
+	return m,err
+}
 
 
 

+ 6 - 2
routers/router.go

@@ -14,7 +14,7 @@ func init()  {
 	beego.Router("/find_password", &controllers.AccountController{},"*:FindPassword")
 
 	beego.Router("/manager", &controllers.ManagerController{},"*:Index")
-	beego.Router("/manager/users", &controllers.ManagerController{},"*:Users")
+	beego.Router("/manager/users", &controllers.ManagerController{})
 	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")
@@ -45,9 +45,13 @@ func init()  {
 	beego.Router("/book/setting/token", &controllers.BookController{},"post:CreateToken")
 	beego.Router("/book/setting/delete", &controllers.BookController{},"post:Delete")
 
-	beego.Router("/book/:key/edit/?:id", &controllers.DocumentController{},"*:Edit")
+	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", &controllers.DocumentController{},"*:Index")
 	beego.Router("/docs/:key/:id", &controllers.DocumentController{},"*:Read")
+
+	beego.Router("/:key/attach_files/:attach_id",&controllers.DocumentController{},"get:DownloadAttachment")
 }

+ 196 - 0
static/css/jstree.css

@@ -0,0 +1,196 @@
+.jstree-contextmenu{
+    z-index: 999999;
+}
+.jstree-contextmenu {
+    z-index: 3000
+}
+
+.jstree-contextmenu.jstree-default-contextmenu {
+    border: 1px solid #d6d6d6;
+    box-shadow: 0 0 8px rgba(99,99,99,.3);
+    background-color: #f6f6f6;
+    padding: 0
+}
+
+.jstree-contextmenu.jstree-default-contextmenu li {
+    display: block
+}
+
+.jstree-contextmenu.jstree-default-contextmenu .vakata-context-separator {
+    display: none
+}
+
+.jstree-contextmenu.jstree-default-contextmenu .vakata-contextmenu-sep {
+    display: none
+}
+
+.jstree-contextmenu.jstree-default-contextmenu li a {
+    border-bottom: 0;
+    height: 30px;
+    line-height: 24px;
+    padding-top: 3px;
+    padding-bottom: 3px
+}
+
+.jstree-contextmenu.jstree-default-contextmenu li.vakata-context-hover a {
+    text-shadow: none;
+    background: #116cd6;
+    box-shadow: none;
+    color: #fff
+}
+
+.jstree-contextmenu.jstree-default-contextmenu li>a>i:empty {
+    width: 24px;
+    height: 24px;
+    line-height: 24px;
+    vertical-align: middle
+}
+
+.jstree .jstree-node .jstree-anchor {
+    padding-right: 36px;
+    color: #666
+}
+
+.jstree .jstree-node .m-tree-operate {
+    position: absolute;
+    right: 6px;
+    z-index: 100;
+    display: block
+}
+
+.jstree .jstree-node .m-tree-operate .operate-show {
+    font-size: 20px;
+    color: #999;
+    display: none
+}
+
+.jstree .jstree-node .m-tree-operate .operate-show i {
+    height: 24px;
+    line-height: 24px;
+    vertical-align: top;
+    margin-top: 1px;
+    display: inline-block;
+    font-size: 18px;
+    transition-property: transform;
+    transition-duration: .3s;
+    transition-timing-function: .3s;
+    transition-delay: 0s;
+    -moz-transition-property: transform;
+    -moz-transition-duration: .3s;
+    -moz-transition-timing-function: .3s;
+    -moz-transition-delay: 0s;
+    -webkit-transition-property: transform;
+    -webkit-transition-duration: .3s;
+    -webkit-transition-timing-function: .3s;
+    -webkit-transition-delay: 0s;
+    -o-transition-property: transform;
+    -o-transition-duration: .3s;
+    -o-transition-timing-function: .3s;
+    -o-transition-delay: 0s
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide {
+    width: 0;
+    right: 100%;
+    top: 2px;
+    position: absolute;
+    overflow: hidden;
+    transition-property: width;
+    transition-duration: .3s;
+    transition-timing-function: linear;
+    transition-delay: 0s;
+    -moz-transition-property: width;
+    -moz-transition-duration: .3s;
+    -moz-transition-timing-function: linear;
+    -moz-transition-delay: 0s;
+    -webkit-transition-property: width;
+    -webkit-transition-duration: .3s;
+    -webkit-transition-timing-function: linear;
+    -webkit-transition-delay: 0s;
+    -o-transition-property: width;
+    -o-transition-duration: .3s;
+    -o-transition-timing-function: linear;
+    -o-transition-delay: 0s
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide b {
+    font-size: 12px;
+    display: inline-block;
+    margin: 3px 2px 0;
+    vertical-align: top;
+    width: 18px;
+    height: 18px;
+    text-align: center;
+    line-height: 18px;
+    border-radius: 9px;
+    color: #fff;
+    cursor: pointer
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide b.add {
+    background-color: #39f
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide b.del {
+    background-color: #c00
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide b.edit {
+    background-color: #e5b120
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide b.up {
+    background-color: #3e8a2a;
+    font-size: 18px
+}
+
+.jstree .jstree-node .m-tree-operate .operate-hide b.down {
+    background-color: #3e8a2a;
+    font-size: 18px
+}
+
+.jstree .jstree-node .m-tree-operate:hover .operate-hide {
+    width: 108px
+}
+
+.jstree .jstree-node .m-tree-operate:hover .operate-show i {
+    color: #333;
+    transform: rotate(360deg);
+    -ms-transform: rotate(360deg);
+    -moz-transform: rotate(360deg);
+    -webkit-transform: rotate(360deg);
+    -o-transform: rotate(360deg)
+}
+
+.jstree .jstree-node .jstree-anchor.jstree-clicked ,.jstree .jstree-hovered{
+    color: #ffffff !important;
+}
+
+.jstree .jstree-node .m-tree-operate.operate-hover .operate-hide {
+    width: 108px
+}
+.jstree-default .jstree-wholerow-clicked {
+    background: #10af88;
+    background: -webkit-linear-gradient(top, #beebff 0%, #a8e4ff 100%);
+    background: linear-gradient(to bottom, #10af88 0%, #10af88 100%);
+    color: #ffffff;
+}
+.jstree-default .jstree-wholerow {
+    height: 30px;
+}
+.jstree-default .jstree-node {
+    min-height: 30px;
+    line-height: 30px;
+    margin-left: 30px;
+    min-width: 30px;
+}
+.jstree-default .jstree-node {
+    min-height: 30px;
+    line-height: 30px;
+    margin-left: 30px;
+    min-width: 30px;
+}
+.jstree-default .jstree-anchor {
+    line-height: 30px;
+    height: 30px;
+}

+ 60 - 2
static/css/markdown.css

@@ -4,9 +4,10 @@ body{
     left: 0;
     right: 0;
     bottom: 0;
-    color: #FAFAFA;
 }
-
+.error-message{
+    color: red;
+}
 .manual-head{
     padding: 5px 5px 5px 5px;
     position: fixed;
@@ -18,6 +19,46 @@ body{
     border-top: 1px solid #DDDDDD;
     bottom: 0;
     top: 40px;
+    background-color: #FAFAFA;
+}
+.manual-category .manual-nav {
+    font-size: 14px;
+    color: #333333;
+    font-weight: 200;
+    zoom:1;
+    border-bottom: 1px solid #ddd
+}
+.manual-category .manual-tree{
+    margin-top: 10px;
+}
+.manual-category .manual-nav .nav-item{
+    font-size: 14px;
+    padding: 0 9px;
+    cursor: pointer;
+    float: left;
+    height: 30px;
+    line-height: 30px;
+    color: #666;
+}
+.manual-category .manual-nav .nav-plus {
+    color: #999;
+    cursor: pointer;
+    height: 24px;
+    width: 24px;
+    line-height: 24px;
+    display: inline-block;
+    margin-top: 4px
+}
+.manual-category .manual-nav .nav-plus:hover{
+    color: #333333;
+}
+.manual-category .manual-nav .nav-item.active{
+    border-bottom: 1px solid #fafafa;
+    margin-bottom: -1px;
+    border-left: 1px solid #ddd;
+    border-right: 1px solid #ddd;
+    padding-left: 8px;
+    padding-right: 8px;
 }
 .manual-editor-container{
     position: absolute;
@@ -26,6 +67,11 @@ body{
     right: 0;
     bottom: 0;
     overflow: hidden;
+    border-top: 1px solid #DDDDDD;
+}
+.manual-editor-container .manual-editormd{
+    position: absolute;
+    bottom: 15px;
 }
 .editormd-group{
     float: left;
@@ -76,4 +122,16 @@ body{
     text-align: center;
     font-family: icomoon,Helvetica,Arial,sans-serif;
     font-style: normal;
+}
+
+.manual-editor-status{
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    height: 30px;
+    overflow: hidden;
+    border-left: 1px solid #DDDDDD;
+    color: #555;
+    background-color: #FAFAFA;
 }

+ 214 - 0
static/editor.md/plugins/file-dialog/file-dialog.js

@@ -0,0 +1,214 @@
+/*!
+ * File (upload) dialog plugin for Editor.md
+ *
+ * @file        file-dialog.js
+ * @author      minho
+ * @version     0.1.0
+ * @updateTime  2017-01-06
+ * {@link       https://github.com/lifei6671/SmartWiki}
+ * @license     MIT
+ */
+
+(function() {
+
+    var factory = function (exports) {
+
+        var pluginName   = "file-dialog";
+
+        exports.fn.fileDialog = function() {
+
+            var _this       = this;
+            var cm          = this.cm;
+            var lang        = this.lang;
+            var editor      = this.editor;
+            var settings    = this.settings;
+            var cursor      = cm.getCursor();
+            var selection   = cm.getSelection();
+            var fileLang   = lang.dialog.file;
+            var classPrefix = this.classPrefix;
+            var iframeName  = classPrefix + "file-iframe";
+            var dialogName  = classPrefix + pluginName, dialog;
+
+            cm.focus();
+
+            var loading = function(show) {
+                var _loading = dialog.find("." + classPrefix + "dialog-mask");
+                _loading[(show) ? "show" : "hide"]();
+            };
+
+            if (editor.find("." + dialogName).length < 1)
+            {
+                var guid   = (new Date).getTime();
+                var action = settings.fileUploadURL + (settings.fileUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;
+
+                if (settings.crossDomainUpload)
+                {
+                    action += "&callback=" + settings.uploadCallbackURL + "&dialog_id=editormd-file-dialog-" + guid;
+                }
+
+                var dialogContent = ( (settings.fileUpload) ? "<form action=\"" + action +"\" target=\"" + iframeName + "\" method=\"post\" enctype=\"multipart/form-data\" class=\"" + classPrefix + "form\">" : "<div class=\"" + classPrefix + "form\">" ) +
+                    ( (settings.fileUpload) ? "<iframe name=\"" + iframeName + "\" id=\"" + iframeName + "\" guid=\"" + guid + "\"></iframe>" : "" ) +
+                    "<label>文件地址</label>" +
+                    "<input type=\"text\" data-url />" + (function(){
+                        return (settings.fileUpload) ? "<div class=\"" + classPrefix + "file-input\">" +
+                            "<input type=\"file\" name=\"" + classPrefix + "file-file\" />" +
+                            "<input type=\"submit\" value=\"本地上传\" />" +
+                            "</div>" : "";
+                    })() +
+                    "<br/>" +
+                    "<label>文件说明</label>" +
+                    "<input type=\"text\" value=\"" + selection + "\" data-alt />" +
+                    "<br/>" +
+                    "<input type='hidden' data-icon>"+
+                    ( (settings.fileUpload) ? "</form>" : "</div>");
+
+                //var imageFooterHTML = "<button class=\"" + classPrefix + "btn " + classPrefix + "image-manager-btn\" style=\"float:left;\">" + fileLang.managerButton + "</button>";
+
+                dialog = this.createDialog({
+                    title      : "文件上传",
+                    width      : (settings.fileUpload) ? 465 : 380,
+                    height     : 254,
+                    name       : dialogName,
+                    content    : dialogContent,
+                    mask       : settings.dialogShowMask,
+                    drag       : settings.dialogDraggable,
+                    lockScreen : settings.dialogLockScreen,
+                    maskStyle  : {
+                        opacity         : settings.dialogMaskOpacity,
+                        backgroundColor : settings.dialogMaskBgColor
+                    },
+                    buttons : {
+                        enter : [lang.buttons.enter, function() {
+                            var url  = this.find("[data-url]").val();
+                            var alt  = this.find("[data-alt]").val();
+                            var icon  = this.find("[data-icon]").val();
+
+                            if (url === "")
+                            {
+                                alert(fileLang.fileURLEmpty);
+                                return false;
+                            }
+
+                            var altAttr = (alt !== "") ? " \"" + alt + "\"" : "";
+
+
+                            if (icon === "" || !icon)
+                            {
+                                cm.replaceSelection("[" + alt + "](" + url + altAttr + ")");
+                            }
+                            else
+                            {
+                                cm.replaceSelection("[![" + alt + "](" + icon +  ")"+ alt +"](" + url + altAttr + ")");
+                            }
+
+
+                            if (alt === "") {
+                                cm.setCursor(cursor.line, cursor.ch + 2);
+                            }
+
+                            this.hide().lockScreen(false).hideMask();
+
+                            return false;
+                        }],
+
+                        cancel : [lang.buttons.cancel, function() {
+                            this.hide().lockScreen(false).hideMask();
+
+                            return false;
+                        }]
+                    }
+                });
+
+                dialog.attr("id", classPrefix + "file-dialog-" + guid);
+
+                if (!settings.fileUpload) {
+                    return ;
+                }
+
+                var fileInput  = dialog.find("[name=\"" + classPrefix + "file-file\"]");
+
+                fileInput.bind("change", function() {
+                    var fileName  = fileInput.val();
+
+                    if (fileName === "")
+                    {
+                        alert(fileLang.uploadFileEmpty);
+
+                        return false;
+                    }
+
+                    loading(true);
+
+                    var submitHandler = function() {
+
+                        var uploadIframe = document.getElementById(iframeName);
+
+                        uploadIframe.onload = function() {
+
+                            loading(false);
+
+                            var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
+                            var json = (body.innerText) ? body.innerText : ( (body.textContent) ? body.textContent : null);
+
+                            json = (typeof JSON.parse !== "undefined") ? JSON.parse(json) : eval("(" + json + ")");
+
+                            if(!settings.crossDomainUpload)
+                            {
+                                if (json.success === 1)
+                                {
+                                    dialog.find("[data-url]").val(json.url);
+
+                                    json.alt && dialog.find("[data-alt]").val(json.alt);
+                                }
+                                else
+                                {
+                                    alert(json.message);
+                                }
+                            }
+
+                            return false;
+                        };
+                    };
+
+                    dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click");
+                });
+            }
+
+            dialog = editor.find("." + dialogName);
+            dialog.find("[type=\"text\"]").val("");
+            dialog.find("[type=\"file\"]").val("");
+
+            this.dialogShowMask(dialog);
+            this.dialogLockScreen();
+            dialog.show();
+
+        };
+
+    };
+
+    // CommonJS/Node.js
+    if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
+    {
+        module.exports = factory;
+    }
+    else if (typeof define === "function")  // AMD/CMD/Sea.js
+    {
+        if (define.amd) { // for Require.js
+
+            define(["editormd"], function(editormd) {
+                factory(editormd);
+            });
+
+        } else { // for Sea.js
+            define(function(require) {
+                var editormd = require("./../../editormd");
+                factory(editormd);
+            });
+        }
+    }
+    else
+    {
+        factory(window.editormd);
+    }
+
+})();

+ 0 - 97
static/editor.md/plugins/video-dialog/video-dialog.js

@@ -1,97 +0,0 @@
-/**
- * 插入视频插件,编辑器必须开启html识别
- *
- * @file video-dialog.js
- * @author Minho
- * @version 0.1
- * @licence MIT
- */
-(function() {
-     var factory = function (exports) {
-
-            var $ = jQuery;
-            var pluginName = "video-dialog";
-
-            exports.fn.videoDialog = function () {
-                var _this = this;
-                var lang = this.lang;
-                var editor = this.editor;
-                var settings = this.settings;
-                var path = settings.pluginPath + pluginName + "/";
-                var classPrefix = this.classPrefix;
-                var dialogName = classPrefix + pluginName, dialog;
-                var dialogLang = lang.dialog.help;
-
-                if (editor.find("." + dialogName).length < 1) {
-
-                    var dialogContent = "";
-
-                    dialog = this.createDialog({
-                        name: dialogName,
-                        title: dialogLang.title,
-                        width: 840,
-                        height: 540,
-                        mask: settings.dialogShowMask,
-                        drag: settings.dialogDraggable,
-                        content: dialogContent,
-                        lockScreen: settings.dialogLockScreen,
-                        maskStyle: {
-                            opacity: settings.dialogMaskOpacity,
-                            backgroundColor: settings.dialogMaskBgColor
-                        },
-                        buttons: {
-                            close: [lang.buttons.close, function () {
-                                this.hide().lockScreen(false).hideMask();
-
-                                return false;
-                            }]
-                        }
-                    });
-                }
-
-                dialog = editor.find("." + dialogName);
-
-                this.dialogShowMask(dialog);
-                this.dialogLockScreen();
-                dialog.show();
-
-                var videoContent = dialog.find(".markdown-body");
-
-                if (videoContent.html() === "") {
-                    $.get(path + "help.md", function (text) {
-                        var md = exports.$marked(text);
-                        videoContent.html(md);
-
-                        videoContent.find("a").attr("target", "_blank");
-                    });
-                }
-            };
-
-        };
-
-    // CommonJS/Node.js
-    if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
-    {
-        module.exports = factory;
-    }
-    else if (typeof define === "function")  // AMD/CMD/Sea.js
-    {
-        if (define.amd) { // for Require.js
-
-            define(["editormd"], function(editormd) {
-                factory(editormd);
-            });
-
-        } else { // for Sea.js
-            define(function(require) {
-                var editormd = require("./../../editormd");
-                factory(editormd);
-            });
-        }
-    }
-    else
-    {
-        factory(window.editormd);
-    }
-})();
-

+ 185 - 13
static/js/markdown.js

@@ -1,3 +1,19 @@
+function showError($msg,$id) {
+    if(!$id){
+        $id = "#form-error-message"
+    }
+    $($id).addClass("error-message").removeClass("success-message").text($msg);
+    return false;
+}
+
+function showSuccess($msg,$id) {
+    if(!$id){
+        $id = "#form-error-message"
+    }
+    $($id).addClass("success-message").removeClass("error-message").text($msg);
+    return true;
+}
+
 $(function () {
     window.editor = editormd("docEditor", {
         width : "100%",
@@ -7,33 +23,31 @@ $(function () {
         placeholder: "本编辑器支持Markdown编辑,左边编写,右边预览",
         imageUpload: true,
         imageFormats: ["jpg", "jpeg", "gif", "png", "JPG", "JPEG", "GIF", "PNG"],
-        imageUploadURL: "/upload",
+        imageUploadURL: window.imageUploadURL ,
         toolbarModes : "full",
         fileUpload: true,
+        fileUploadURL : window.fileUploadURL,
         taskList : true,
         flowChart : true,
         htmlDecode : "style,script,iframe,title,onmouseover,onmouseout,style",
         lineNumbers : false,
-        fileUploadURL : '/upload',
+
         tocStartLevel : 1,
         tocm : true,
         saveHTMLToTextarea : true,
         onload : function() {
             this.hideToolbar();
-           // this.videoDialog();
-            //this.executePlugin("video-dialog","video-dialog/video-dialog");
         }
     });
+    editormd.loadPlugin("/static/editor.md/plugins/file-dialog/file-dialog");
 
-    editormd.loadPlugin("/static/editor.md/plugins/video-dialog/video-dialog", function(){
-        //editormd.videoDialog();
-    });
-
+    /**
+     * 实现标题栏操作
+     */
     $("#editormd-tools").on("click","a[class!='disabled']",function () {
        var name = $(this).find("i").attr("name");
-
        if(name === "attachment"){
-
+            window.editor.fileDialog();
        }else if(name === "history"){
 
        }else if(name === "sidebar"){
@@ -67,9 +81,6 @@ $(function () {
                }
                cm.replaceSelection(selectionText.join("\n"));
            }
-       }else if(name === "video"){
-           //插入视频
-           window.editor.videoDialog();
        }else {
            var action = window.editor.toolbarHandlers[name];
 
@@ -79,4 +90,165 @@ $(function () {
            }
        }
    }) ;
+
+    //实现小提示
+    $("[data-toggle='tooltip']").hover(function () {
+        var title = $(this).attr('data-title');
+        var direction = $(this).attr("data-direction");
+        var tips = 3;
+        if(direction === "top"){
+            tips = 1;
+        }else if(direction === "right"){
+            tips = 2;
+        }else if(direction === "bottom"){
+            tips = 3;
+        }else if(direction === "left"){
+            tips = 4;
+        }
+        index = layer.tips(title, this, {
+            tips: tips
+        });
+    }, function () {
+        layer.close(index);
+    });
+
+    $("#btnAddDocument").on("click",function () {
+        $("#addDocumentModal").modal("show");
+    });
+    $("#addDocumentModal").on("show.bs.modal",function () {
+        window.addDocumentModalFormHtml = $(this).find("form").html();
+    }).on("hidden.bs.modal",function () {
+       $(this).find("form").html(window.addDocumentModalFormHtml);
+    });
+
+    function loadDocument($node) {
+        var index = layer.load(1, {
+            shade: [0.1,'#fff'] //0.1透明度的白色背景
+        });
+
+        $.get("/docs/"+ window.book.identify +"/" + $node.node.id ).done(function (data) {
+            win.isEditorChange = true;
+            layer.close(index);
+            $("#documentId").val(selected.node.id);
+            window.editor.clear();
+            if(data.errcode === 0 && data.data.doc.content){
+                window.editor.insertValue(data.data.doc.content);
+                window.editor.setCursor({line:0, ch:0});
+            }else if(data.errcode !== 0){
+                layer.msg("文档加载失败");
+            }
+        }).fail(function () {
+            layer.close(index);
+            layer.msg("文档加载失败");
+        });
+    }
+    /**
+     * 添加文档
+     */
+    $("#addDocumentForm").ajaxForm({
+        beforeSubmit : function () {
+            var doc_name = $.trim($("#documentName").val());
+            if (doc_name === ""){
+                return showError("目录名称不能为空","#add-error-message")
+            }
+            return true;
+        },
+        success : function (res) {
+            if(res.errcode === 0){
+                var data = { "id" : res.data.doc_id,'parent' : res.data.parent_id,"text" : res.data.doc_name};
+
+                var node = window.treeCatalog.get_node(data.id);
+                if(node){
+                    window.treeCatalog.rename_node({"id":data.id},data.text);
+                }else {
+                    var result = window.treeCatalog.create_node(res.data.parent_id, data, 'last');
+                    window.treeCatalog.deselect_all();
+                    window.treeCatalog.select_node(data);
+                    window.editor.clear();
+                }
+                $("#markdown-save").removeClass('change').addClass('disabled');
+                $("#addDocumentModal").modal('hide');
+            }else{
+                showError(res.message,"#add-error-message")
+            }
+        },
+        error :function () {
+
+        }
+    });
+
+    /**
+     * 文档目录树
+     */
+    $("#sidebar").jstree({
+        'plugins': ["wholerow", "types", 'dnd', 'contextmenu'],
+        "types": {
+            "default": {
+                "icon": false  // 删除默认图标
+            }
+        },
+        'core': {
+            'check_callback': true,
+            "multiple": false,
+            'animation': 0,
+            "data": window.documentCategory
+        },
+        "contextmenu": {
+            show_at_node: false,
+            select_node: false,
+            "items": {
+                "添加文档": {
+                    "separator_before": false,
+                    "separator_after": true,
+                    "_disabled": false,
+                    "label": "添加文档",
+                    "icon": "fa fa-plus",
+                    "action": function (data) {
+
+                        var inst = $.jstree.reference(data.reference),
+                            node = inst.get_node(data.reference);
+
+                        openCreateCatalogDialog(node);
+                    }
+                },
+                "编辑": {
+                    "separator_before": false,
+                    "separator_after": true,
+                    "_disabled": false,
+                    "label": "编辑",
+                    "icon": "fa fa-edit",
+                    "action": function (data) {
+                        var inst = $.jstree.reference(data.reference);
+                        var node = inst.get_node(data.reference);
+                        editDocumentDialog(node);
+                    }
+                },
+                "删除": {
+                    "separator_before": false,
+                    "separator_after": true,
+                    "_disabled": false,
+                    "label": "删除",
+                    "icon": "fa fa-trash-o",
+                    "action": function (data) {
+                        var inst = $.jstree.reference(data.reference);
+                        var node = inst.get_node(data.reference);
+                        deleteDocumentDialog(node);
+                    }
+                }
+            }
+        }
+    }).on('loaded.jstree', function () {
+        window.treeCatalog = $(this).jstree();
+        var $select_node_id = window.treeCatalog.get_selected();
+        if($select_node_id) {
+            var $select_node = window.treeCatalog.get_node($select_node_id[0])
+            if ($select_node) {
+                $select_node.node = {
+                    id: $select_node.id
+                };
+
+                loadDocument($select_node);
+            }
+        }
+    });
 });

+ 1 - 0
uploads/.gitignore

@@ -0,0 +1 @@
+# Created by .ignore support plugin (hsz.mobi)

BIN
uploads/201704/b4c17ca29fe7b7f4dec402d7dd7543c6_100.png


+ 55 - 30
views/document/markdown_edit_template.tpl

@@ -6,12 +6,19 @@
     <meta name="viewport" content="width=device-width, initial-scale=1">
 
     <title>编辑文档 - Powered by MinDoc</title>
-    <script type="text/javascript">window.editor = null;</script>
+    <script type="text/javascript">
+        window.editor = null;
+        window.imageUploadURL = "{{urlfor "DocumentController.Upload" "identify" .Model.Identify}}";
+        window.fileUploadURL = "{{urlfor "DocumentController.Upload" "identify" .Model.Identify}}";
+        window.documentCategory = {{.Result}};
+        window.book = {{.ModelResult}};
+    </script>
     <!-- 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/editor.md/css/editormd.css" rel="stylesheet">
+    <link href="/static/css/jstree.css" rel="stylesheet">
     <link href="/static/css/markdown.css" rel="stylesheet">
     <!-- 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:// -->
@@ -61,7 +68,6 @@
             <a href="javascript:;" data-toggle="tooltip" data-title="添加表格"><i class="fa fa-table item" name="table" unselectable="on"></i></a>
             <a href="javascript:;" data-toggle="tooltip" data-title="引用"><i class="fa fa-quote-right item" name="quote" unselectable="on"></i></a>
             <a href="javascript:;" data-toggle="tooltip" data-title="GFM 任务列表"><i class="fa fa-tasks item" name="tasks" aria-hidden="true"></i></a>
-            <a href="javascript:;" data-toggle="tooltip" data-title="插入视频"><i class="fa fa-file-video-o" name="video" aria-hidden="true"></i></a>
             <a href="javascript:;" data-toggle="tooltip" data-title="附件"><i class="fa fa-paperclip last" aria-hidden="true" name="attachment"></i></a>
         </div>
 
@@ -84,46 +90,65 @@
     </div>
     <div class="manual-body">
         <div class="manual-category" id="manualCategory">
+            <div class="manual-nav">
+                <div class="nav-item active"><i class="fa fa-bars" aria-hidden="true"></i> 目录</div>
+                <div class="nav-plus pull-right" id="btnAddDocument" data-toggle="tooltip" data-title="创建目录" data-direction="right"><i class="fa fa-plus" aria-hidden="true"></i></div>
+                <div class="clearfix"></div>
+            </div>
+            <div class="manual-tree" id="sidebar">
 
+            </div>
         </div>
         <div class="manual-editor-container" id="manualEditorContainer">
-            <div id="docEditor"></div>
+            <div class="manual-editormd" id="docEditor"></div>
+            <div class="manual-editor-status">
+
+            </div>
         </div>
+
     </div>
 </div>
+<!-- Modal -->
+<div class="modal fade" id="addDocumentModal" tabindex="-1" role="dialog" aria-labelledby="addDocumentModalLabel">
+    <div class="modal-dialog" role="document">
+        <form method="post" action="{{urlfor "DocumentController.Create" ":key" .Model.Identify}}" id="addDocumentForm" class="form-horizontal">
+            <input type="hidden" name="identify" value="{{.Model.Identify}}">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <h4 class="modal-title" id="myModalLabel">添加目录</h4>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label class="col-sm-2 control-label">目录名称 <span class="error-message">*</span></label>
+                    <div class="col-sm-10">
+                        <input type="text" name="doc_name" id="documentName" placeholder="目录名称" class="form-control"  maxlength="50">
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-sm-2 control-label">目录标识</label>
+                    <div class="col-sm-10">
+                        <input type="text" name="doc_identify" id="documentIdentify" placeholder="目录唯一标识" class="form-control" maxlength="50">
+                        <p style="color: #999;font-size: 12px;">文档标识只能包含小写字母、数字,以及“-”和“_”符号,并且只能小写字母开头</p>
+                    </div>
 
+                </div>
+            </div>
+            <div class="modal-footer">
+                <span id="add-error-message" class="error-message"></span>
+                <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
+                <button type="submit" class="btn btn-primary" id="btnSaveDocument" data-loading-text="保存中...">立即保存</button>
+            </div>
+        </div>
+        </form>
+    </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 src="/static/editor.md/editormd.js" type="text/javascript"></script>
-<script src="/static/editor.md/plugins/video-dialog/video-dialog.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/markdown.js" type="text/javascript"></script>
-<script type="text/javascript">
-    $(function () {
-        $("#sidebar").jstree({
-            'plugins':["wholerow","types"],
-            "types": {
-                "default" : {
-                    "icon" : false  // 删除默认图标
-                }
-            },
-            'core' : {
-                'check_callback' : false,
-                "multiple" : false ,
-                'animation' : 0
-            }
-        });
-
-        $("[data-toggle='tooltip']").hover(function () {
-            var title = $(this).attr('data-title');
-                index = layer.tips(title,this,{
-                    tips : 3
-                });
-            },function () {
-            layer.close(index);
-        });
-    });
-</script>
 </body>
 </html>