Procházet zdrojové kódy

优化pdf生成逻辑

Minho před 8 roky
rodič
revize
837d0da991

+ 0 - 4
controllers/book.go

@@ -534,11 +534,7 @@ func (c *BookController) Release() {
 	}
 	go func(identify string) {
 		models.NewDocument().ReleaseContent(book_id)
-		pdfpath := "cache/" + identify + ".pdf"
 
-		if _,err := os.Stat(pdfpath); os.IsExist(err){
-			os.Remove(pdfpath)
-		}
 
 	}(identify)
 

+ 39 - 10
controllers/document.go

@@ -16,7 +16,7 @@ import (
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/orm"
-	"github.com/lifei6671/godoc/utils"
+	"github.com/lifei6671/godoc/utils/wkhtmltopdf"
 )
 
 //DocumentController struct.
@@ -162,11 +162,16 @@ func (c *DocumentController) Read() {
 	if doc.BookId != bookResult.BookId {
 		c.Abort("403")
 	}
+	attach,err := models.NewAttachment().FindListByDocumentId(doc.DocumentId)
+	if err == nil {
+		doc.AttachList = attach
+	}
+
 	if c.IsAjax() {
 		var data struct{
 			DocTitle string `json:"doc_title"`
 			Body string	`json:"body"`
-			Title string `json:"title"`
+			Title string 	`json:"title"`
 		}
 		data.DocTitle = doc.DocumentName
 		data.Body = doc.Release
@@ -459,8 +464,19 @@ func (c *DocumentController) Upload()  {
 		"attach" : attachment,
 	}
 
-	c.Data["json"] = result
-	c.ServeJSON(true)
+	//c.Data["json"] = result
+	//c.ServeJSON(true)
+	//c.StopRun()
+	//
+	//returnJSON, err := json.Marshal(result)
+	//
+	//if err != nil {
+	//	beego.Error(err)
+	//}
+	//
+	//c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
+	//fmt.Fprint(c.Ctx.ResponseWriter,string(returnJSON))
+	c.Ctx.Output.JSON(result,true,false)
 	c.StopRun()
 }
 
@@ -746,14 +762,27 @@ func (c *DocumentController) Export() {
 		if _,err := os.Stat(pdfpath); os.IsNotExist(err){
 			paths := make([]string, len(docs))
 			index := 0
-			for e := pathList.Front(); e != nil; e = e.Next() {
-				paths[index] = e.Value.(string)
-				index ++
+
+			pdfg, err := wkhtmltopdf.NewPDFGenerator()
+
+			if err != nil {
+				beego.Error(err)
+				c.Abort("500")
 			}
 
-			beego.Info(paths,pdfpath)
+			for e := pathList.Front(); e != nil; e = e.Next() {
+				pdfg.AddPage(wkhtmltopdf.NewPage(paths[index]))
+			}
+			err = pdfg.Create()
+			if err != nil {
+				beego.Error(err)
+				c.Abort("500")
+			}
 
-			utils.ConverterHtmlToPdf(paths, pdfpath)
+			err = pdfg.WriteFile(pdfpath)
+			if err != nil {
+				beego.Error(err)
+			}
 		}
 
 		c.Ctx.Output.Download(pdfpath, identify + ".pdf")
@@ -788,7 +817,7 @@ func RecursiveFun(parent_id int,prefix,dpath  string,c *DocumentController,book
 				beego.Error(err)
 				c.Abort("500")
 			}
-			beego.Info(fpath,html)
+			//beego.Info(fpath,html)
 			f.WriteString(html)
 			f.Close()
 

+ 1 - 0
models/book.go

@@ -259,6 +259,7 @@ 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()
 

+ 24 - 3
models/document.go

@@ -6,6 +6,8 @@ import (
 	"github.com/lifei6671/godoc/conf"
 	"github.com/astaxie/beego/orm"
 	"github.com/astaxie/beego"
+	"bytes"
+	"fmt"
 )
 
 // Document struct.
@@ -121,12 +123,31 @@ 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()
+	var docs []*Document
+	_,err := o.QueryTable(m.TableNameWithPrefix()).Filter("book_id",book_id).All(&docs,"document_id","content")
 
 	if err != nil {
-		beego.Error(err)
+		beego.Error("发布失败 => ",err)
+		return
+	}
+	for _, item := range docs {
+		item.Release = item.Content
+		attach_list ,err := NewAttachment().FindListByDocumentId(item.DocumentId)
+		if err == nil && len(attach_list) > 0 {
+			content := bytes.NewBufferString("<div class=\"attach-list\"><strong>附件</strong><ul>")
+			for _,attach := range attach_list {
+				li := fmt.Sprintf("<li><a href=\"%s\" target=\"_blank\" title=\"%s\">%s</a></li>",attach.HttpPath,attach.FileName,attach.FileName)
+
+				content.WriteString(li)
+			}
+			content.WriteString("</ul></div>")
+			item.Release +=  content.String()
+		}
+		_,err = o.Update(item,"release")
+		if err != nil {
+			beego.Error(fmt.Sprintf("发布失败 => %+v",item),err)
+		}
 	}
-
 }
 
 func (m *Document)  FindListByBookId(book_id int) (docs []*Document,err error) {

+ 6 - 1
static/css/kancloud.css

@@ -332,7 +332,7 @@ h6 {
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
-    color: #7e888b
+    color: #444
 }
 .manual-article .article-content{
     min-width: 980px;
@@ -350,6 +350,11 @@ h6 {
     min-height: 90px;
     padding: 5px;
 }
+.manual-article .article-content .article-body .attach-list{
+    list-style: none;
+    border-top: 1px solid #DDDDDD;
+    padding-top: 15px;
+}
 .m-manual .manual-progress {
     position: fixed;
     top: 54px;

+ 2 - 1
static/css/markdown.css

@@ -33,11 +33,12 @@ body{
     width: 280px;
     position: fixed;
     border-top: 1px solid #DDDDDD;
-    bottom: 15px;
+    bottom: 0px;
     top: 40px;
     background-color: #FAFAFA;
     left: 0;
     right: 0;
+    padding-bottom: 15px;
     overflow-y:auto;
 }
 .manual-category .manual-nav {

+ 1 - 6
static/js/editor.js

@@ -145,17 +145,12 @@ function pushDocumentCategory($node) {
  * @param $lists
  */
 function pushVueLists($lists) {
-    for(var i in window.vueApp.lists){
-        var item = window.vueApp.lists[i];
-        window.vueApp.lists.$remove(item);
-    }
 
+    window.vueApp.lists = [];
     for(var j in $lists){
         var item = $lists[j];
         window.vueApp.lists.push(item);
     }
-    console.log(window.vueApp.lists)
-    $("#attachInfo").text(" " + window.vueApp.lists.length + " 个附件")
 }
 
 //实现小提示

+ 2 - 4
static/js/markdown.js

@@ -52,8 +52,6 @@ $(function () {
             resetEditorChanged(true);
         }
     });
-    editormd.loadPlugin("/static/editor.md/plugins/file-dialog/file-dialog");
-
 
     /**
      * 实现标题栏操作
@@ -61,7 +59,7 @@ $(function () {
     $("#editormd-tools").on("click","a[class!='disabled']",function () {
        var name = $(this).find("i").attr("name");
        if(name === "attachment"){
-            window.editor.fileDialog();
+           $("#uploadAttachModal").modal("show");
        }else if(name === "history"){
 
        }else if(name === "save"){
@@ -145,7 +143,7 @@ $(function () {
                 var node = { "id" : res.data.doc_id,'parent' : res.data.parent_id === 0 ? '#' : res.data.parent_id ,"text" : res.data.doc_name,"identify" : res.data.identify,"version" : res.data.version};
                 pushDocumentCategory(node);
                 window.selectNode = node;
-
+                pushVueLists(res.data.attach);
             }else{
                 layer.msg("文档加载失败");
             }

+ 0 - 77
utils/pdf_linux.go

@@ -1,77 +0,0 @@
-package utils
-
-import (
-	"os/exec"
-	"bufio"
-	"io"
-	"io/ioutil"
-	"errors"
-
-	"github.com/astaxie/beego"
-)
-
-
-// 使用 wkhtmltopdf 是实现 html 转 pdf.
-// 中文说明:http://www.jianshu.com/p/4d65857ffe5e#
-func ConverterHtmlToPdf(uri []string,path string) (error) {
-	exe := beego.AppConfig.String("wkhtmltopdf")
-
-	if exe == "" {
-		return errors.New("wkhtmltopdf not exist.")
-	}
-	params := []string{"--margin-bottom","25"}
-
-	params = append(params,uri...)
-	params = append(params,path)
-
-	beego.Info(params)
-
-	cmd := exec.Command(exe,params...)
-
-	stdout, err := cmd.StdoutPipe()
-
-	if err != nil {
-		return errors.New("StdoutPipe: " + err.Error())
-	}
-	stderr, err := cmd.StderrPipe()
-	if err != nil {
-
-		return errors.New("StderrPipe: " + err.Error())
-	}
-
-	if err := cmd.Start(); err != nil {
-
-		return errors.New("Start: "+ err.Error())
-	}
-
-	reader := bufio.NewReader(stdout)
-
-	//实时循环读取输出流中的一行内容
-	for {
-		line ,err2 := reader.ReadString('\n')
-
-		if err2 != nil || io.EOF == err2 {
-			break
-		}
-
-		beego.Info(line)
-	}
-
-	bytesErr, err := ioutil.ReadAll(stderr)
-
-	if err == nil {
-		beego.Info(string(bytesErr))
-	}else{
-		beego.Error("Error: Stderr => " + err.Error())
-		return err
-	}
-
-	if err := cmd.Wait(); err != nil {
-
-		beego.Error("Error: ", err.Error())
-
-		return err
-	}
-
-	return nil
-}

+ 0 - 81
utils/pdf_windows.go

@@ -1,81 +0,0 @@
-package utils
-
-import (
-	"os/exec"
-	"bufio"
-	"io"
-	"io/ioutil"
-	"errors"
-
-	"github.com/axgle/mahonia"
-	"github.com/astaxie/beego"
-)
-
-// 使用 wkhtmltopdf 是实现 html 转 pdf.
-// 中文说明:http://www.jianshu.com/p/4d65857ffe5e#
-func ConverterHtmlToPdf(uri []string,path string) (error) {
-
-	exe := beego.AppConfig.String("wkhtmltopdf")
-
-	if exe == "" {
-		return errors.New("wkhtmltopdf not exist.")
-	}
-
-	params := []string{"/C",exe,"--margin-bottom","25"}
-
-	params = append(params,uri...)
-	params = append(params,path)
-
-	beego.Info(params)
-
-	cmd := exec.Command("cmd",params...)
-
-	stdout, err := cmd.StdoutPipe()
-
-	if err != nil {
-		return errors.New("StdoutPipe: " + err.Error())
-	}
-	stderr, err := cmd.StderrPipe()
-	if err != nil {
-
-		return errors.New("StderrPipe: " + err.Error())
-	}
-
-	if err := cmd.Start(); err != nil {
-
-		return errors.New("Start: "+ err.Error())
-	}
-
-	reader := bufio.NewReader(stdout)
-	enc := mahonia.NewDecoder("gbk")
-
-	//实时循环读取输出流中的一行内容
-	for {
-		line ,err2 := reader.ReadString('\n')
-
-		if err2 != nil || io.EOF == err2 {
-			break
-		}
-
-		beego.Info(enc.ConvertString(line))
-	}
-
-	bytesErr, err := ioutil.ReadAll(stderr)
-
-	if err == nil {
-		beego.Info(enc.ConvertString(string(bytesErr)))
-	}else{
-		beego.Error("Error: Stderr => " + err.Error())
-		return err
-	}
-
-	if err := cmd.Wait(); err != nil {
-
-		beego.Error("Error: ", err.Error())
-
-		return err
-	}
-
-	return nil
-
-}

+ 443 - 0
utils/wkhtmltopdf/options.go

@@ -0,0 +1,443 @@
+package wkhtmltopdf
+
+import (
+	"fmt"
+	"reflect"
+)
+
+//A list of options that can be set from code to make it easier to see which options are available
+type globalOptions struct {
+	CookieJar         stringOption //Read and write cookies from and to the supplied cookie jar file
+	Copies            uintOption   //Number of copies to print into the pdf file (default 1)
+	Dpi               uintOption   //Change the dpi explicitly (this has no effect on X11 based systems)
+	ExtendedHelp      boolOption   //Display more extensive help, detailing less common command switches
+	Grayscale         boolOption   //PDF will be generated in grayscale
+	Help              boolOption   //Display help
+	HTMLDoc           boolOption   //Output program html help
+	ImageDpi          uintOption   //When embedding images scale them down to this dpi (default 600)
+	ImageQuality      uintOption   //When jpeg compressing images use this quality (default 94)
+	License           boolOption   //Output license information and exit
+	Lowquality        boolOption   //Generates lower quality pdf/ps. Useful to shrink the result document space
+	ManPage           boolOption   //Output program man page
+	MarginBottom      uintOption   //Set the page bottom margin
+	MarginLeft        uintOption   //Set the page left margin (default 10mm)
+	MarginRight       uintOption   //Set the page right margin (default 10mm)
+	MarginTop         uintOption   //Set the page top margin
+	Orientation       stringOption // Set orientation to Landscape or Portrait (default Portrait)
+	NoCollate         boolOption   //Do not collate when printing multiple copies (default collate)
+	PageHeight        uintOption   //Page height
+	PageSize          stringOption //Set paper size to: A4, Letter, etc. (default A4)
+	PageWidth         uintOption   //Page width
+	NoPdfCompression  boolOption   //Do not use lossless compression on pdf objects
+	Quiet             boolOption   //Be less verbose
+	ReadArgsFromStdin boolOption   //Read command line arguments from stdin
+	Readme            boolOption   //Output program readme
+	Title             stringOption //The title of the generated pdf file (The title of the first document is used if not specified)
+	Version           boolOption   //Output version information and exit
+}
+
+func (gopt *globalOptions) Args() []string {
+	return optsToArgs(gopt)
+}
+
+type outlineOptions struct {
+	DumpDefaultTocXsl boolOption   //Dump the default TOC xsl style sheet to stdout
+	DumpOutline       stringOption //Dump the outline to a file
+	NoOutline         boolOption   //Do not put an outline into the pdf
+	OutlineDepth      uintOption   //Set the depth of the outline (default 4)
+}
+
+func (oopt *outlineOptions) Args() []string {
+	return optsToArgs(oopt)
+}
+
+type pageOptions struct {
+	Allow                     sliceOption  //Allow the file or files from the specified folder to be loaded (repeatable)
+	NoBackground              boolOption   //Do not print background
+	CacheDir                  stringOption //Web cache directory
+	CheckboxCheckedSvg        stringOption //Use this SVG file when rendering checked checkboxes
+	CheckboxSvg               stringOption //Use this SVG file when rendering unchecked checkboxes
+	Cookie                    mapOption    //Set an additional cookie (repeatable), value should be url encoded
+	CustomHeader              mapOption    //Set an additional HTTP header (repeatable)
+	CustomHeaderPropagation   boolOption   //Add HTTP headers specified by --custom-header for each resource request
+	NoCustomHeaderPropagation boolOption   //Do not add HTTP headers specified by --custom-header for each resource request
+	DebugJavascript           boolOption   //Show javascript debugging output
+	DefaultHeader             boolOption   //Add a default header, with the name of the page to the left, and the page number to the right, this is short for: --header-left='[webpage]' --header-right='[page]/[toPage]' --top 2cm --header-line
+	Encoding                  stringOption //Set the default text encoding, for input
+	DisableExternalLinks      boolOption   //Do not make links to remote web pages
+	EnableForms               boolOption   //Turn HTML form fields into pdf form fields
+	NoImages                  boolOption   //Do not load or print images
+	DisableInternalLinks      boolOption   //Do not make local links
+	DisableJavascript         boolOption   //Do not allow web pages to run javascript
+	JavascriptDelay           uintOption   //Wait some milliseconds for javascript finish (default 200)
+	LoadErrorHandling         stringOption //Specify how to handle pages that fail to load: abort, ignore or skip (default abort)
+	LoadMediaErrorHandling    stringOption //Specify how to handle media files that fail to load: abort, ignore or skip (default ignore)
+	DisableLocalFileAccess    boolOption   //Do not allowed conversion of a local file to read in other local files, unless explicitly allowed with --allow
+	MinimumFontSize           uintOption   //Minimum font size
+	ExcludeFromOutline        boolOption   //Do not include the page in the table of contents and outlines
+	PageOffset                uintOption   //Set the starting page number (default 0)
+	Password                  stringOption //HTTP Authentication password
+	EnablePlugins             boolOption   //Enable installed plugins (plugins will likely not work)
+	Post                      mapOption    //Add an additional post field (repeatable)
+	PostFile                  mapOption    //Post an additional file (repeatable)
+	PrintMediaType            boolOption   //Use print media-type instead of screen
+	Proxy                     stringOption //Use a proxy
+	RadiobuttonCheckedSvg     stringOption //Use this SVG file when rendering checked radiobuttons
+	RadiobuttonSvg            stringOption //Use this SVG file when rendering unchecked radiobuttons
+	RunScript                 sliceOption  //Run this additional javascript after the page is done loading (repeatable)
+	DisableSmartShrinking     boolOption   //Disable the intelligent shrinking strategy used by WebKit that makes the pixel/dpi ratio none constant
+	NoStopSlowScripts         boolOption   //Do not Stop slow running javascripts
+	EnableTocBackLinks        boolOption   //Link from section header to toc
+	UserStyleSheet            stringOption //Specify a user style sheet, to load with every page
+	Username                  stringOption //HTTP Authentication username
+	ViewportSize              stringOption //Set viewport size if you have custom scrollbars or css attribute overflow to emulate window size
+	WindowStatus              stringOption //Wait until window.status is equal to this string before rendering page
+	Zoom                      floatOption  //Use this zoom factor (default 1)
+}
+
+func (popt *pageOptions) Args() []string {
+	return optsToArgs(popt)
+}
+
+type headerAndFooterOptions struct {
+	FooterCenter   stringOption //Centered footer text
+	FooterFontName stringOption //Set footer font name (default Arial)
+	FooterFontSize uintOption   //Set footer font size (default 12)
+	FooterHTML     stringOption //Adds a html footer
+	FooterLeft     stringOption //Left aligned footer text
+	FooterLine     boolOption   //Display line above the footer
+	FooterRight    stringOption //Right aligned footer text
+	FooterSpacing  floatOption  //Spacing between footer and content in mm (default 0)
+	HeaderCenter   stringOption //Centered header text
+	HeaderFontName stringOption //Set header font name (default Arial)
+	HeaderFontSize uintOption   //Set header font size (default 12)
+	HeaderHTML     stringOption //Adds a html header
+	HeaderLeft     stringOption //Left aligned header text
+	HeaderLine     boolOption   //Display line below the header
+	HeaderRight    stringOption //Right aligned header text
+	HeaderSpacing  floatOption  //Spacing between header and content in mm (default 0)
+	Replace        mapOption    //Replace [name] with value in header and footer (repeatable)
+}
+
+func (hopt *headerAndFooterOptions) Args() []string {
+	return optsToArgs(hopt)
+}
+
+type tocOptions struct {
+	DisableDottedLines  boolOption   //Do not use dotted lines in the toc
+	TocHeaderText       stringOption //The header text of the toc (default Table of Contents)
+	TocLevelIndentation uintOption   //For each level of headings in the toc indent by this length (default 1em)
+	DisableTocLinks     boolOption   //Do not link from toc to sections
+	TocTextSizeShrink   floatOption  //For each level of headings in the toc the font is scaled by this factor
+	XslStyleSheet       stringOption //Use the supplied xsl style sheet for printing the table of content
+}
+
+func (topt *tocOptions) Args() []string {
+	return optsToArgs(topt)
+}
+
+type argParser interface {
+	Parse() []string //Used in the cmd call
+}
+
+type stringOption struct {
+	option string
+	value  string
+}
+
+func (so stringOption) Parse() []string {
+	args := []string{}
+	if so.value == "" {
+		return args
+	}
+	args = append(args, "--"+so.option)
+	args = append(args, so.value)
+	return args
+}
+
+func (so *stringOption) Set(value string) {
+	so.value = value
+}
+
+type sliceOption struct {
+	option string
+	value  []string
+}
+
+func (so sliceOption) Parse() []string {
+	args := []string{}
+	if len(so.value) == 0 {
+		return args
+	}
+	for _, v := range so.value {
+		args = append(args, "--"+so.option)
+		args = append(args, v)
+	}
+	return args
+}
+
+func (so *sliceOption) Set(value string) {
+	so.value = append(so.value, value)
+}
+
+type mapOption struct {
+	option string
+	value  map[string]string
+}
+
+func (mo mapOption) Parse() []string {
+	args := []string{}
+	if mo.value == nil || len(mo.value) == 0 {
+		return args
+	}
+	for k, v := range mo.value {
+		args = append(args, "--"+mo.option)
+		args = append(args, k)
+		args = append(args, v)
+	}
+	return args
+}
+
+func (mo *mapOption) Set(key, value string) {
+	if mo.value == nil {
+		mo.value = make(map[string]string)
+	}
+	mo.value[key] = value
+}
+
+type uintOption struct {
+	option string
+	value  uint
+	isSet  bool
+}
+
+func (io uintOption) Parse() []string {
+	args := []string{}
+	if io.isSet == false {
+		return args
+	}
+	args = append(args, "--"+io.option)
+	args = append(args, fmt.Sprintf("%d", io.value))
+	return args
+}
+
+func (io *uintOption) Set(value uint) {
+	io.isSet = true
+	io.value = value
+}
+
+type floatOption struct {
+	option string
+	value  float64
+	isSet  bool
+}
+
+func (fo floatOption) Parse() []string {
+	args := []string{}
+	if fo.isSet == false {
+		return args
+	}
+	args = append(args, "--"+fo.option)
+	args = append(args, fmt.Sprintf("%.3f", fo.value))
+	return args
+}
+
+func (fo *floatOption) Set(value float64) {
+	fo.isSet = true
+	fo.value = value
+}
+
+type boolOption struct {
+	option string
+	value  bool
+}
+
+func (bo boolOption) Parse() []string {
+	if bo.value {
+		return []string{"--" + bo.option}
+	}
+	return []string{}
+}
+
+func (bo *boolOption) Set(value bool) {
+	bo.value = value
+}
+
+func newGlobalOptions() globalOptions {
+	return globalOptions{
+		CookieJar:         stringOption{option: "cookie-jar"},
+		Copies:            uintOption{option: "copies"},
+		Dpi:               uintOption{option: "dpi"},
+		ExtendedHelp:      boolOption{option: "extended-help"},
+		Grayscale:         boolOption{option: "grayscale"},
+		Help:              boolOption{option: "true"},
+		HTMLDoc:           boolOption{option: "htmldoc"},
+		ImageDpi:          uintOption{option: "image-dpi"},
+		ImageQuality:      uintOption{option: "image-quality"},
+		License:           boolOption{option: "license"},
+		Lowquality:        boolOption{option: "lowquality"},
+		ManPage:           boolOption{option: "manpage"},
+		MarginBottom:      uintOption{option: "margin-bottom"},
+		MarginLeft:        uintOption{option: "margin-left"},
+		MarginRight:       uintOption{option: "margin-right"},
+		MarginTop:         uintOption{option: "margin-top"},
+		Orientation:       stringOption{option: "orientation"},
+		NoCollate:         boolOption{option: "nocollate"},
+		PageHeight:        uintOption{option: "page-height"},
+		PageSize:          stringOption{option: "page-size"},
+		PageWidth:         uintOption{option: "page-width"},
+		NoPdfCompression:  boolOption{option: "no-pdf-compression"},
+		Quiet:             boolOption{option: "quiet"},
+		ReadArgsFromStdin: boolOption{option: "read-args-from-stdin"},
+		Readme:            boolOption{option: "readme"},
+		Title:             stringOption{option: "title"},
+		Version:           boolOption{option: "version"},
+	}
+}
+
+func newOutlineOptions() outlineOptions {
+	return outlineOptions{
+		DumpDefaultTocXsl: boolOption{option: "dump-default-toc-xsl"},
+		DumpOutline:       stringOption{option: "dump-outline"},
+		NoOutline:         boolOption{option: "no-outline"},
+		OutlineDepth:      uintOption{option: "outline-depth"},
+	}
+}
+
+func newPageOptions() pageOptions {
+	return pageOptions{
+		Allow:                     sliceOption{option: "allow"},
+		NoBackground:              boolOption{option: "no-background"},
+		CacheDir:                  stringOption{option: "cache-dir"},
+		CheckboxCheckedSvg:        stringOption{option: "checkbox-checked-svg"},
+		CheckboxSvg:               stringOption{option: "checkbox-svg"},
+		Cookie:                    mapOption{option: "cookie"},
+		CustomHeader:              mapOption{option: "custom-header"},
+		CustomHeaderPropagation:   boolOption{option: "custom-header-propagation"},
+		NoCustomHeaderPropagation: boolOption{option: "no-custom-header-propagation"},
+		DebugJavascript:           boolOption{option: "debug-javascript"},
+		DefaultHeader:             boolOption{option: "default-header"},
+		Encoding:                  stringOption{option: "encoding"},
+		DisableExternalLinks:      boolOption{option: "disable-external-links"},
+		EnableForms:               boolOption{option: "enable-forms"},
+		NoImages:                  boolOption{option: "no-images"},
+		DisableInternalLinks:      boolOption{option: "disable-internal-links"},
+		DisableJavascript:         boolOption{option: "disable-javascript "},
+		JavascriptDelay:           uintOption{option: "javascript-delay"},
+		LoadErrorHandling:         stringOption{option: "load-error-handling"},
+		LoadMediaErrorHandling:    stringOption{option: "load-media-error-handling"},
+		DisableLocalFileAccess:    boolOption{option: "disable-local-file-access"},
+		MinimumFontSize:           uintOption{option: "minimum-font-size"},
+		ExcludeFromOutline:        boolOption{option: "exclude-from-outline"},
+		PageOffset:                uintOption{option: "page-offset"},
+		Password:                  stringOption{option: "password"},
+		EnablePlugins:             boolOption{option: "enable-plugins"},
+		Post:                      mapOption{option: "post"},
+		PostFile:                  mapOption{option: "post-file"},
+		PrintMediaType:            boolOption{option: "print-media-type"},
+		Proxy:                     stringOption{option: "proxy"},
+		RadiobuttonCheckedSvg: stringOption{option: "radiobutton-checked-svg"},
+		RadiobuttonSvg:        stringOption{option: "radiobutton-svg"},
+		RunScript:             sliceOption{option: "run-script"},
+		DisableSmartShrinking: boolOption{option: "disable-smart-shrinking"},
+		NoStopSlowScripts:     boolOption{option: "no-stop-slow-scripts"},
+		EnableTocBackLinks:    boolOption{option: "enable-toc-back-links"},
+		UserStyleSheet:        stringOption{option: "user-style-sheet"},
+		Username:              stringOption{option: "username"},
+		ViewportSize:          stringOption{option: "viewport-size"},
+		WindowStatus:          stringOption{option: "window-status"},
+		Zoom:                  floatOption{option: "zoom"},
+	}
+}
+
+func newHeaderAndFooterOptions() headerAndFooterOptions {
+	return headerAndFooterOptions{
+		FooterCenter:   stringOption{option: "footer-center"},
+		FooterFontName: stringOption{option: "footer-font-name"},
+		FooterFontSize: uintOption{option: "footer-font-size"},
+		FooterHTML:     stringOption{option: "footer-html"},
+		FooterLeft:     stringOption{option: "footer-left"},
+		FooterLine:     boolOption{option: "footer-line"},
+		FooterRight:    stringOption{option: "footer-right"},
+		FooterSpacing:  floatOption{option: "footer-spacing"},
+		HeaderCenter:   stringOption{option: "header-center"},
+		HeaderFontName: stringOption{option: "header-font-name"},
+		HeaderFontSize: uintOption{option: "header-font-size"},
+		HeaderHTML:     stringOption{option: "header-html"},
+		HeaderLeft:     stringOption{option: "header-left"},
+		HeaderLine:     boolOption{option: "header-line"},
+		HeaderRight:    stringOption{option: "header-right"},
+		HeaderSpacing:  floatOption{option: "header-spacing"},
+		Replace:        mapOption{option: "replace"},
+	}
+}
+
+func newTocOptions() tocOptions {
+	return tocOptions{
+		DisableDottedLines:  boolOption{option: "disable-dotted-lines"},
+		TocHeaderText:       stringOption{option: "toc-header-text"},
+		TocLevelIndentation: uintOption{option: "toc-level-indentation"},
+		DisableTocLinks:     boolOption{option: "disable-toc-links"},
+		TocTextSizeShrink:   floatOption{option: "toc-text-size-shrink"},
+		XslStyleSheet:       stringOption{option: "xsl-style-sheet"},
+	}
+}
+
+func optsToArgs(opts interface{}) []string {
+	args := []string{}
+	rv := reflect.Indirect(reflect.ValueOf(opts))
+	if rv.Kind() != reflect.Struct {
+		return args
+	}
+	for i := 0; i < rv.NumField(); i++ {
+		prsr, ok := rv.Field(i).Interface().(argParser)
+		if ok {
+			s := prsr.Parse()
+			if len(s) > 0 {
+				args = append(args, s...)
+			}
+		}
+	}
+	return args
+}
+
+// Constants for orientation modes
+const (
+	OrientationLandscape = "Landscape" // Landscape mode
+	OrientationPortrait  = "Portrait"  // Portrait mode
+)
+
+// Constants for page sizes
+const (
+	PageSizeA0        = "A0"        //	841 x 1189 mm
+	PageSizeA1        = "A1"        //	594 x 841 mm
+	PageSizeA2        = "A2"        //	420 x 594 mm
+	PageSizeA3        = "A3"        //	297 x 420 mm
+	PageSizeA4        = "A4"        //	210 x 297 mm, 8.26
+	PageSizeA5        = "A5"        //	148 x 210 mm
+	PageSizeA6        = "A6"        //	105 x 148 mm
+	PageSizeA7        = "A7"        //	74 x 105 mm
+	PageSizeA8        = "A8"        //	52 x 74 mm
+	PageSizeA9        = "A9"        //	37 x 52 mm
+	PageSizeB0        = "B0"        //	1000 x 1414 mm
+	PageSizeB1        = "B1"        //	707 x 1000 mm
+	PageSizeB2        = "B2"        //	500 x 707 mm
+	PageSizeB3        = "B3"        //	353 x 500 mm
+	PageSizeB4        = "B4"        //	250 x 353 mm
+	PageSizeB5        = "B5"        //	176 x 250 mm, 6.93
+	PageSizeB6        = "B6"        //	125 x 176 mm
+	PageSizeB7        = "B7"        //	88 x 125 mm
+	PageSizeB8        = "B8"        //	62 x 88 mm
+	PageSizeB9        = "B9"        //	33 x 62 mm
+	PageSizeB10       = "B10"       //	31 x 44 mm
+	PageSizeC5E       = "C5E"       //	163 x 229 mm
+	PageSizeComm10E   = "Comm10E"   //	105 x 241 mm, U.S. Common 10 Envelope
+	PageSizeDLE       = "DLE"       //	110 x 220 mm
+	PageSizeExecutive = "Executive" //	7.5 x 10 inches, 190.5 x 254 mm
+	PageSizeFolio     = "Folio"     //	210 x 330 mm
+	PageSizeLedger    = "Ledger"    //	431.8 x 279.4 mm
+	PageSizeLegal     = "Legal"     //	8.5 x 14 inches, 215.9 x 355.6 mm
+	PageSizeLetter    = "Letter"    //	8.5 x 11 inches, 215.9 x 279.4 mm
+	PageSizeTabloid   = "Tabloid"   //	279.4 x 431.8 mm
+	PageSizeCustom    = "Custom"    //	Unknown, or a user defined size.
+)

+ 294 - 0
utils/wkhtmltopdf/wkhtmltopdf.go

@@ -0,0 +1,294 @@
+// Package wkhtmltopdf contains wrappers around the wkhtmltopdf commandline tool
+package wkhtmltopdf
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+var binPath string //the cached paths as used by findPath()
+
+// SetPath sets the path to wkhtmltopdf
+func SetPath(path string) {
+	binPath = path
+}
+
+// GetPath gets the path to wkhtmltopdf
+func GetPath() string {
+	return binPath
+}
+
+// Page is the input struct for each page
+type Page struct {
+	Input string
+	PageOptions
+}
+
+// InputFile returns the input string and is part of the page interface
+func (p *Page) InputFile() string {
+	return p.Input
+}
+
+// Args returns the argument slice and is part of the page interface
+func (p *Page) Args() []string {
+	return p.PageOptions.Args()
+}
+
+// Reader returns the io.Reader and is part of the page interface
+func (p *Page) Reader() io.Reader {
+	return nil
+}
+
+// NewPage creates a new input page from a local or web resource (filepath or URL)
+func NewPage(input string) *Page {
+	return &Page{
+		Input:       input,
+		PageOptions: NewPageOptions(),
+	}
+}
+
+// PageReader is one input page (a HTML document) that is read from an io.Reader
+// You can add only one Page from a reader
+type PageReader struct {
+	Input io.Reader
+	PageOptions
+}
+
+// InputFile returns the input string and is part of the page interface
+func (pr *PageReader) InputFile() string {
+	return "-"
+}
+
+// Args returns the argument slice and is part of the page interface
+func (pr *PageReader) Args() []string {
+	return pr.PageOptions.Args()
+}
+
+//Reader returns the io.Reader and is part of the page interface
+func (pr *PageReader) Reader() io.Reader {
+	return pr.Input
+}
+
+// NewPageReader creates a new PageReader from an io.Reader
+func NewPageReader(input io.Reader) *PageReader {
+	return &PageReader{
+		Input:       input,
+		PageOptions: NewPageOptions(),
+	}
+}
+
+type page interface {
+	Args() []string
+	InputFile() string
+	Reader() io.Reader
+}
+
+// PageOptions are options for each input page
+type PageOptions struct {
+	pageOptions
+	headerAndFooterOptions
+}
+
+// Args returns the argument slice
+func (po *PageOptions) Args() []string {
+	return append(append([]string{}, po.pageOptions.Args()...), po.headerAndFooterOptions.Args()...)
+}
+
+// NewPageOptions returns a new PageOptions struct with all options
+func NewPageOptions() PageOptions {
+	return PageOptions{
+		pageOptions:            newPageOptions(),
+		headerAndFooterOptions: newHeaderAndFooterOptions(),
+	}
+}
+
+// cover page
+type cover struct {
+	Input string
+	pageOptions
+}
+
+// table of contents
+type toc struct {
+	Include bool
+	allTocOptions
+}
+
+type allTocOptions struct {
+	pageOptions
+	tocOptions
+}
+
+// PDFGenerator is the main wkhtmltopdf struct, always use NewPDFGenerator to obtain a new PDFGenerator struct
+type PDFGenerator struct {
+	globalOptions
+	outlineOptions
+
+	Cover      cover
+	TOC        toc
+	OutputFile string //filename to write to, default empty (writes to internal buffer)
+
+	binPath string
+	outbuf  bytes.Buffer
+	pages   []page
+}
+
+//Args returns the commandline arguments as a string slice
+func (pdfg *PDFGenerator) Args() []string {
+	args := []string{}
+	args = append(args, pdfg.globalOptions.Args()...)
+	args = append(args, pdfg.outlineOptions.Args()...)
+	if pdfg.Cover.Input != "" {
+		args = append(args, "cover")
+		args = append(args, pdfg.Cover.Input)
+		args = append(args, pdfg.Cover.pageOptions.Args()...)
+	}
+	if pdfg.TOC.Include {
+		args = append(args, "toc")
+		args = append(args, pdfg.TOC.pageOptions.Args()...)
+		args = append(args, pdfg.TOC.tocOptions.Args()...)
+	}
+	for _, page := range pdfg.pages {
+		args = append(args, "page")
+		args = append(args, page.InputFile())
+		args = append(args, page.Args()...)
+	}
+	if pdfg.OutputFile != "" {
+		args = append(args, pdfg.OutputFile)
+	} else {
+		args = append(args, "-")
+	}
+	return args
+}
+
+// ArgString returns Args as a single string
+func (pdfg *PDFGenerator) ArgString() string {
+	return strings.Join(pdfg.Args(), " ")
+}
+
+// AddPage adds a new input page to the document.
+// A page is an input HTML page, it can span multiple pages in the output document.
+// It is a Page when read from file or URL or a PageReader when read from memory.
+func (pdfg *PDFGenerator) AddPage(p page) {
+	pdfg.pages = append(pdfg.pages, p)
+}
+
+// SetPages resets all pages
+func (pdfg *PDFGenerator) SetPages(p []page) {
+	pdfg.pages = p
+}
+
+// Buffer returns the embedded output buffer used if OutputFile is empty
+func (pdfg *PDFGenerator) Buffer() *bytes.Buffer {
+	return &pdfg.outbuf
+}
+
+// Bytes returns the output byte slice from the output buffer used if OutputFile is empty
+func (pdfg *PDFGenerator) Bytes() []byte {
+	return pdfg.outbuf.Bytes()
+}
+
+// WriteFile writes the contents of the output buffer to a file
+func (pdfg *PDFGenerator) WriteFile(filename string) error {
+	return ioutil.WriteFile(filename, pdfg.Bytes(), 0666)
+}
+
+//findPath finds the path to wkhtmltopdf by
+//- first looking in the current dir
+//- looking in the PATH and PATHEXT environment dirs
+//- using the WKHTMLTOPDF_PATH environment dir
+//The path is cached, meaning you can not change the location of wkhtmltopdf in
+//a running program once it has been found
+func (pdfg *PDFGenerator) findPath() error {
+	const exe = "wkhtmltopdf"
+	if binPath != "" {
+		pdfg.binPath = binPath
+		return nil
+	}
+	exeDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
+	if err != nil {
+		return err
+	}
+	path, err := exec.LookPath(filepath.Join(exeDir, exe))
+	if err == nil && path != "" {
+		binPath = path
+		pdfg.binPath = path
+		return nil
+	}
+	path, err = exec.LookPath(exe)
+	if err == nil && path != "" {
+		binPath = path
+		pdfg.binPath = path
+		return nil
+	}
+	dir := os.Getenv("WKHTMLTOPDF_PATH")
+	if dir == "" {
+		return fmt.Errorf("%s not found", exe)
+	}
+	path, err = exec.LookPath(filepath.Join(dir, exe))
+	if err == nil && path != "" {
+		binPath = path
+		pdfg.binPath = path
+		return nil
+	}
+	return fmt.Errorf("%s not found", exe)
+}
+
+// Create creates the PDF document and stores it in the internal buffer if no error is returned
+func (pdfg *PDFGenerator) Create() error {
+	return pdfg.run()
+}
+
+func (pdfg *PDFGenerator) run() error {
+
+	errbuf := &bytes.Buffer{}
+
+	cmd := exec.Command(pdfg.binPath, pdfg.Args()...)
+
+	cmd.Stdout = &pdfg.outbuf
+	cmd.Stderr = errbuf
+	//if there is a pageReader page (from Stdin) we set Stdin to that reader
+	for _, page := range pdfg.pages {
+		if page.Reader() != nil {
+			cmd.Stdin = page.Reader()
+			break
+		}
+	}
+
+	err := cmd.Run()
+	if err != nil {
+		errStr := errbuf.String()
+		if strings.TrimSpace(errStr) == "" {
+			errStr = err.Error()
+		}
+		return errors.New(errStr)
+	}
+	return nil
+}
+
+// NewPDFGenerator returns a new PDFGenerator struct with all options created and
+// checks if wkhtmltopdf can be found on the system
+func NewPDFGenerator() (*PDFGenerator, error) {
+	pdfg := &PDFGenerator{
+		globalOptions:  newGlobalOptions(),
+		outlineOptions: newOutlineOptions(),
+		Cover: cover{
+			pageOptions: newPageOptions(),
+		},
+		TOC: toc{
+			allTocOptions: allTocOptions{
+				tocOptions:  newTocOptions(),
+				pageOptions: newPageOptions(),
+			},
+		},
+	}
+	err := pdfg.findPath()
+	return pdfg, err
+}

+ 4 - 4
views/document/default_read.tpl

@@ -53,7 +53,7 @@
                         {{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>*/}}
+                        <li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "pdf"}}" target="_blank">项目导出PDF</a> </li>
                         {{end}}
 
                         <li><a href="{{urlfor "HomeController.Index"}}" title="返回首页">返回首页</a> </li>
@@ -77,7 +77,7 @@
                 <div class="tab-wrap">
                     <div class="tab-item manual-catalog">
                         <div class="catalog-list read-book-preview" id="sidebar">
-{{.Result}}
+                        {{.Result}}
                         </div>
 
                     </div>
@@ -104,12 +104,12 @@
                             </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>
+                    <!--
                     {{/*
                     {{if .Model.IsDisplayComment}}
                     <div id="articleComment" class="m-comment">
@@ -155,7 +155,7 @@
                         </div>
                     </div>
                     {{end}}
-*/}}
+*/}}-->
                 </div>
 
             </div>

+ 35 - 18
views/document/html_edit_template.tpl

@@ -84,7 +84,7 @@
                 <div id="htmlEditor" class="manual-editormd-active" style="height: 100%"></div>
             </div>
             <div class="manual-editor-status">
-                <div id="attachInfo" class="item"></div>
+                <div id="attachInfo" class="item">0 个附件</div>
             </div>
         </div>
 
@@ -153,13 +153,13 @@
                                     </div>
                                 </template>
                                 <template v-else-if="item.state == 'error'">
-                                    <div class="error-message text">
-                                        上传失败
-                                    </div>
+                                    <span class="error-message">${item.message}</span>
+                                    <button type="button" class="btn btn-sm close" @click="removeAttach(item.attachment_id)">
+                                        <i class="fa fa-remove" aria-hidden="true"></i>
+                                    </button>
                                 </template>
                                 <template v-else>
-                                    <input type="hidden" name="attach_id[0]" :value="item.attach_id">
-                                    <input type="text" class="form-control" placeholder="附件名称" :value="item.file_name">
+                                    <a :href="item.http_path" target="_blank" :title="item.file_name">${item.file_name}</a>
                                     <span class="text">(${(item.file_size/1024/1024).toFixed(4)}MB)</span>
                                     <span class="error-message">${item.message}</span>
                                     <button type="button" class="btn btn-sm close" @click="removeAttach(item.attachment_id)">
@@ -174,7 +174,7 @@
                 <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="button" class="btn btn-primary" id="btnUploadAttachFile">确定</button>
+                    <button type="button" class="btn btn-primary" id="btnUploadAttachFile" data-dismiss="modal">确定</button>
                 </div>
             </div>
         </form>
@@ -202,8 +202,17 @@
         delimiters : ['${','}'],
         methods : {
             removeAttach : function ($attach_id) {
-                console.log($attach_id);
                 var $this = this;
+                var item = $this.lists.filter(function ($item) {
+                   return $item.attachment_id == $attach_id;
+                });
+
+                if(item && item[0].hasOwnProperty("state")){
+                   $this.lists = $this.lists.filter(function ($item) {
+                       return $item.attachment_id != $attach_id;
+                   });
+                   return;
+                }
                 $.ajax({
                     url : "{{urlfor "DocumentController.RemoveAttachment"}}",
                     type : "post",
@@ -211,21 +220,23 @@
                     success : function (res) {
                         console.log(res);
                         if(res.errcode === 0){
-                            for(var i in $this.lists){
-                                var item = $this.lists[i];
-                                if (item.attachment_id == res.data.attachment_id){
-                                    $this.lists.$remove(item);
-                                    break;
-                                }
-                            }
+                            $this.lists = $this.lists.filter(function ($item) {
+                                return $item.attachment_id != $attach_id;
+                            });
                         }else{
                             layer.msg(res.message);
                         }
                     }
                 });
             }
+        },
+        watch : {
+            lists : function ($lists) {
+                $("#attachInfo").text(" " + $lists.length + " 个附件")
+            }
         }
     });
+
 </script>
 <script src="/static/js/editor.js" type="text/javascript"></script>
 <script src="/static/js/html-editor.js" type="text/javascript"></script>
@@ -267,16 +278,22 @@
                            var item = window.vueApp.lists[i];
                            if(item.attachment_id == file.id){
                                item.state = "error";
+                               item.message = "上传失败";
+                               break;
                            }
                        }
 
                     }).on("uploadSuccess",function (file, res) {
-                        console.log(file);
 
                         for(var index in window.vueApp.lists){
                             var item = window.vueApp.lists[index];
-                            if(item.id === file.id){
-                                window.vueApp.lists.splice(index,1,res.attach);
+                            if(item.attachment_id === file.id){
+                                if(res.errcode === 0) {
+                                    window.vueApp.lists.splice(index, 1, res.attach);
+                                }else{
+                                    item.message = res.message;
+                                    item.state = "error";
+                                }
                                 break;
                             }
                         }

+ 178 - 4
views/document/markdown_edit_template.tpl

@@ -25,7 +25,8 @@
     <link href="{{cdncss "/static/editor.md/css/editormd.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/jstree.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/highlight/styles/zenburn.css"}}" rel="stylesheet">
-    <link href="{{cdncss "/static/css/markdown.css"}}" rel="stylesheet">
+    <link href="{{cdncss "/static/webuploader/webuploader.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:// -->
     <!--[if lt IE 9]>
@@ -111,7 +112,7 @@
                 <div id="docEditor" class="manual-editormd-active"></div>
             </div>
             <div class="manual-editor-status">
-
+                <div id="attachInfo" class="item">0 个附件</div>
             </div>
         </div>
 
@@ -154,13 +155,186 @@
         </form>
     </div>
 </div>
+<div class="modal fade" id="uploadAttachModal" tabindex="-1" role="dialog" aria-labelledby="uploadAttachModalLabel">
+    <div class="modal-dialog" role="document">
+        <form method="post" id="uploadAttachModalForm" class="form-horizontal">
+            <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="attach-drop-panel">
+                        <div class="upload-container" id="filePicker"><i class="fa fa-upload" aria-hidden="true"></i></div>
+                    </div>
+                    <div class="attach-list" id="attachList">
+                        <template v-for="item in lists">
+                            <div class="attach-item" :id="item.attachment_id">
+                                <template v-if="item.state == 'wait'">
+                                    <div class="progress">
+                                        <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100">
+                                            <span class="sr-only">0% Complete (success)</span>
+                                        </div>
+                                    </div>
+                                </template>
+                                <template v-else-if="item.state == 'error'">
+                                    <span class="error-message">${item.message}</span>
+                                    <button type="button" class="btn btn-sm close" @click="removeAttach(item.attachment_id)">
+                                        <i class="fa fa-remove" aria-hidden="true"></i>
+                                    </button>
+                                </template>
+                                <template v-else>
+                                    <a :href="item.http_path" target="_blank" :title="item.file_name">${item.file_name}</a>
+                                    <span class="text">(${(item.file_size/1024/1024).toFixed(4)}MB)</span>
+                                    <span class="error-message">${item.message}</span>
+                                    <button type="button" class="btn btn-sm close" @click="removeAttach(item.attachment_id)">
+                                        <i class="fa fa-remove" aria-hidden="true"></i>
+                                    </button>
+                                    <div class="clearfix"></div>
+                                </template>
+                            </div>
+                        </template>
+                    </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="button" class="btn btn-primary" id="btnUploadAttachFile" data-dismiss="modal">确定</button>
+                </div>
+            </div>
+        </form>
+    </div>
+</div>
+
 <script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
+<script src="{{cdnjs "/static/vuejs/vue.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
+<script src="{{cdnjs "/static/webuploader/webuploader.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/jstree/3.3.4/jstree.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/editor.md/editormd.js"}}" type="text/javascript"></script>
 <script type="text/javascript" src="{{cdnjs "/static/layer/layer.js"}}"></script>
 <script src="{{cdnjs "/static/js/jquery.form.js"}}" type="text/javascript"></script>
-<script src="{{cdnjs "/static/js/editor.js"}}" type="text/javascript"></script>
-<script src="{{cdnjs "/static/js/markdown.js"}}" type="text/javascript"></script>
+<script type="text/javascript">
+    window.vueApp = new Vue({
+        el : "#attachList",
+        data : {
+            lists : []
+        },
+        delimiters : ['${','}'],
+        methods : {
+            removeAttach : function ($attach_id) {
+                var $this = this;
+                var item = $this.lists.filter(function ($item) {
+                    return $item.attachment_id == $attach_id;
+                });
+
+                if(item && item[0].hasOwnProperty("state")){
+                    $this.lists = $this.lists.filter(function ($item) {
+                        return $item.attachment_id != $attach_id;
+                    });
+                    return;
+                }
+                $.ajax({
+                    url : "{{urlfor "DocumentController.RemoveAttachment"}}",
+                    type : "post",
+                    data : { "attach_id" : $attach_id},
+                    success : function (res) {
+                        console.log(res);
+                        if(res.errcode === 0){
+                            $this.lists = $this.lists.filter(function ($item) {
+                                return $item.attachment_id != $attach_id;
+                            });
+                        }else{
+                            layer.msg(res.message);
+                        }
+                    }
+                });
+            }
+        },
+        watch : {
+            lists : function ($lists) {
+                $("#attachInfo").text(" " + $lists.length + " 个附件")
+            }
+        }
+    });
+
+</script>
+<script src="/static/js/editor.js" type="text/javascript"></script>
+<script src="/static/js/markdown.js" type="text/javascript"></script>
+<script type="text/javascript">
+    $(function () {
+        $("#attachInfo").on("click",function () {
+            $("#uploadAttachModal").modal("show");
+        });
+        window.uploader = null;
+
+        $("#uploadAttachModal").on("shown.bs.modal",function () {
+            if(window.uploader === null){
+                try {
+                    window.uploader = WebUploader.create({
+                        auto: true,
+                        dnd : true,
+                        swf: '/static/webuploader/Uploader.swf',
+                        server: '{{urlfor "DocumentController.Upload"}}',
+                        formData : { "identify" : {{.Model.Identify}},"doc_id" :  window.selectNode.id },
+                        pick: "#filePicker",
+                        fileVal : "editormd-file-file",
+                        fileNumLimit : 1,
+                        compress : false
+                    }).on("beforeFileQueued",function (file) {
+                        uploader.reset();
+                    }).on( 'fileQueued', function( file ) {
+                        var item = {
+                            state : "wait",
+                            attachment_id : file.id,
+                            file_size : file.size,
+                            file_name : file.name,
+                            message : "正在上传"
+                        };
+                        window.vueApp.lists.splice(0,0,item);
+
+                    }).on("uploadError",function (file,reason) {
+                        for(var i in window.vueApp.lists){
+                            var item = window.vueApp.lists[i];
+                            if(item.attachment_id == file.id){
+                                item.state = "error";
+                                item.message = "上传失败";
+                                break;
+                            }
+                        }
+
+                    }).on("uploadSuccess",function (file, res) {
+
+                        for(var index in window.vueApp.lists){
+                            var item = window.vueApp.lists[index];
+                            if(item.attachment_id === file.id){
+                                if(res.errcode === 0) {
+                                    window.vueApp.lists.splice(index, 1, res.attach);
+
+                                }else{
+                                    item.message = res.message;
+                                    item.state = "error";
+                                }
+                                break;
+                            }
+                        }
+
+                    }).on("beforeFileQueued",function (file) {
+
+                    }).on("uploadComplete",function () {
+
+                    }).on("uploadProgress",function (file, percentage) {
+                        var $li = $( '#'+file.id ),
+                            $percent = $li.find('.progress .progress-bar');
+
+                        $percent.css( 'width', percentage * 100 + '%' );
+                    });
+                }catch(e){
+                    console.log(e);
+                }
+            }
+        });
+    });
+</script>
 </body>
 </html>