1、实现富文本编辑器

2、实现文档转换为PDF、MOBI、EPUB格式
pull/219/head
Minho 2018-01-25 19:18:59 +08:00
parent dab6f31d01
commit e1ec6bb788
16 changed files with 990 additions and 473 deletions

201
LICENSE
View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -109,7 +109,15 @@ func (c *BaseController) ExecuteViewPathTemplate(tplName string,data interface{}
}
func (c *BaseController) BaseUrl() string {
return c.Ctx.Input.Scheme() + "://" + c.Ctx.Request.Host
baseUrl := beego.AppConfig.DefaultString("baseurl","")
if baseUrl != "" {
if strings.HasSuffix(baseUrl,"/"){
baseUrl = strings.TrimSuffix(baseUrl,"/")
}
}else{
baseUrl = c.Ctx.Input.Scheme() + "://" + c.Ctx.Request.Host
}
return baseUrl
}
//显示错误信息页面.

View File

@ -25,7 +25,6 @@ import (
"github.com/lifei6671/mindoc/conf"
"github.com/lifei6671/mindoc/models"
"github.com/lifei6671/mindoc/utils"
"github.com/lifei6671/mindoc/utils/wkhtmltopdf"
"github.com/russross/blackfriday"
)
@ -68,7 +67,7 @@ func isReadable(identify, token string, c *DocumentController) *models.BookResul
}
}
bookResult := book.ToBookResult()
bookResult := models.NewBookResult().ToBookResult(*book)
if c.Member != nil {
rel, err := models.NewRelationship().FindByBookIdAndMemberId(bookResult.BookId, c.Member.MemberId)
@ -283,7 +282,7 @@ func (c *DocumentController) Edit() {
c.JsonResult(6002, "项目不存在或权限不足")
}
bookResult = book.ToBookResult()
bookResult = models.NewBookResult().ToBookResult(*book)
} else {
bookResult, err = models.NewBookResult().FindByIdentify(identify, c.Member.MemberId)
@ -545,7 +544,7 @@ func (c *DocumentController) Upload() {
}
if attachment.HttpPath == "" {
attachment.HttpPath = beego.URLFor("DocumentController.DownloadAttachment", ":key", identify, ":attach_id", attachment.AttachmentId)
attachment.HttpPath = c.BaseUrl() + beego.URLFor("DocumentController.DownloadAttachment", ":key", identify, ":attach_id", attachment.AttachmentId)
if err := attachment.Update(); err != nil {
beego.Error("SaveToFile => ", err)
@ -845,13 +844,6 @@ func (c *DocumentController) Content() {
c.JsonResult(0, "ok", doc)
}
func (c *DocumentController) ExportDoc() {
c.Export(true)
}
func (c *DocumentController) ExportBook() {
c.Export(false)
}
func (c *DocumentController) GetDocumentById(id string) (doc *models.Document, err error) {
doc = models.NewDocument()
@ -871,7 +863,7 @@ func (c *DocumentController) GetDocumentById(id string) (doc *models.Document, e
}
// 导出
func (c *DocumentController) Export(single_doc bool) {
func (c *DocumentController) Export() {
c.Prepare()
c.TplName = "document/export.tpl"
@ -897,7 +889,7 @@ func (c *DocumentController) Export(single_doc bool) {
c.Abort("500")
}
bookResult = book.ToBookResult()
bookResult = models.NewBookResult().ToBookResult(*book)
} else {
bookResult = isReadable(identify, token, c)
}
@ -906,76 +898,50 @@ func (c *DocumentController) Export(single_doc bool) {
// TODO: 私有项目禁止导出
}
docs, err := models.NewDocument().FindListByBookId(bookResult.BookId)
if !strings.HasPrefix(bookResult.Cover,"http:://") && !strings.HasPrefix(bookResult.Cover,"https:://"){
bookResult.Cover = c.BaseUrl() + bookResult.Cover
}
eBookResult,err := bookResult.Converter(c.CruSession.SessionID())
if err != nil {
beego.Error(err)
beego.Error("转换文档失败:" + bookResult.BookName + " -> " + err.Error())
c.Abort("500")
}
if output == "pdf" {
exe := beego.AppConfig.String("wkhtmltopdf")
if exe == "" {
c.TplName = "errors/error.tpl"
c.Data["ErrorMessage"] = "没有配置PDF导出程序"
c.Data["ErrorCode"] = 50010
return
c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".pdf")
//如果没有开启缓存则10分钟后删除
if !bookResult.IsCacheEBook {
defer func(pdfpath string) {
time.Sleep(time.Minute * 10)
os.Remove(filepath.Dir(pdfpath))
}(eBookResult.PDFPath)
}
c.StopRun()
}else if output == "epub" {
c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".epub")
dpath := "cache/" + bookResult.Identify
os.MkdirAll(dpath, 0766)
pathList := list.New()
// 增加对单页文档的导出dandycheung, 2017-12-07
if single_doc {
id := c.Ctx.Input.Param(":id")
if doc, err := c.GetDocumentById(id); err == nil {
EachFun("", dpath, c, bookResult, doc, pathList)
//如果没有开启缓存则10分钟后删除
if !bookResult.IsCacheEBook {
defer func(pdfpath string) {
time.Sleep(time.Minute * 10)
os.Remove(filepath.Dir(pdfpath))
}(eBookResult.EpubPath)
}
} else {
RecursiveFun(0, "", dpath, c, bookResult, docs, pathList)
c.StopRun()
}else if output == "mobi" {
c.Ctx.Output.Download(eBookResult.PDFPath, identify + ".epub")
//如果没有开启缓存则10分钟后删除
if !bookResult.IsCacheEBook {
defer func(pdfpath string) {
time.Sleep(time.Minute * 10)
os.Remove(filepath.Dir(pdfpath))
}(eBookResult.MobiPath)
}
defer os.RemoveAll(dpath)
// TODO: check if the pathList is empty
os.MkdirAll("./cache", 0766)
pdfpath := filepath.Join("cache", identify+"_"+c.CruSession.SessionID()+".pdf")
if _, err := os.Stat(pdfpath); os.IsNotExist(err) {
wkhtmltopdf.SetPath(beego.AppConfig.String("wkhtmltopdf"))
pdfg, err := wkhtmltopdf.NewPDFGenerator()
if err != nil {
beego.Error(err)
c.Abort("500")
}
pdfg.MarginBottom.Set(35)
for e := pathList.Front(); e != nil; e = e.Next() {
if page, ok := e.Value.(string); ok {
pdfg.AddPage(wkhtmltopdf.NewPage(page))
}
}
err = pdfg.Create()
if err != nil {
beego.Error(err)
c.Abort("500")
}
err = pdfg.WriteFile(pdfpath)
if err != nil {
beego.Error(err)
}
}
c.Ctx.Output.Download(pdfpath, identify+".pdf")
defer os.Remove(pdfpath)
c.StopRun()
}

View File

@ -0,0 +1,497 @@
//Author:TruthHun
//Email:TruthHun@QQ.COM
//Date:2018-01-21
package converter
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"os/exec"
"errors"
"github.com/TruthHun/gotil/cryptil"
"github.com/TruthHun/gotil/filetil"
"github.com/TruthHun/gotil/ziptil"
)
type Converter struct {
BasePath string
Config Config
Debug bool
GeneratedCover string
}
//目录结构
type Toc struct {
Id int `json:"id"`
Link string `json:"link"`
Pid int `json:"pid"`
Title string `json:"title"`
}
//config.json文件解析结构
type Config struct {
Charset string `json:"charset"` //字符编码默认utf-8编码
Cover string `json:"cover"` //封面图片或者封面html文件
Timestamp string `json:"date"` //时间日期,如“2018-01-01 12:12:21”其实是time.Time格式但是直接用string就好
Description string `json:"description"` //摘要
Footer string `json:"footer"` //pdf的footer
Header string `json:"header"` //pdf的header
Identifier string `json:"identifier"` //即uuid留空即可
Language string `json:"language"` //语言如zh、en、zh-CN、en-US等
Creator string `json:"creator"` //作者即author
Publisher string `json:"publisher"` //出版单位
Contributor string `json:"contributor"` //同Publisher
Title string `json:"title"` //文档标题
Format []string `json:"format"` //导出格式可选值pdf、epub、mobi
FontSize string `json:"font_size"` //默认的pdf导出字体大小
PaperSize string `json:"paper_size"` //页面大小
MarginLeft string `json:"margin_left"` //PDF文档左边距写数字即可默认72pt
MarginRight string `json:"margin_right"` //PDF文档左边距写数字即可默认72pt
MarginTop string `json:"margin_top"` //PDF文档左边距写数字即可默认72pt
MarginBottom string `json:"margin_bottom"` //PDF文档左边距写数字即可默认72pt
More []string `json:"more"` //更多导出选项[PDF导出选项具体参考https://manual.calibre-ebook.com/generated/en/ebook-convert.html#pdf-output-options]
Toc []Toc `json:"toc"` //目录
///////////////////////////////////////////
Order []string `json:"-"` //这个不需要赋值
}
var (
output = "output" //文档导出文件夹
ebookConvert = "ebook-convert"
)
// 接口文档 https://manual.calibre-ebook.com/generated/en/ebook-convert.html#table-of-contents
//根据json配置文件创建文档转化对象
func NewConverter(configFile string, debug ...bool) (converter *Converter, err error) {
var (
cfg Config
basepath string
db bool
)
if len(debug) > 0 {
db = debug[0]
}
if cfg, err = parseConfig(configFile); err == nil {
if basepath, err = filepath.Abs(filepath.Dir(configFile)); err == nil {
//设置默认值
if len(cfg.Timestamp) == 0 {
cfg.Timestamp = time.Now().Format("2006-01-02 15:04:05")
}
if len(cfg.Charset) == 0 {
cfg.Charset = "utf-8"
}
converter = &Converter{
Config: cfg,
BasePath: basepath,
Debug: db,
}
}
}
return
}
//执行文档转换
func (this *Converter) Convert() (err error) {
if !this.Debug { //调试模式下不删除生成的文件
defer this.converterDefer() //最后移除创建的多余而文件
}
if err = this.generateMimeType(); err != nil {
return
}
if err = this.generateMetaInfo(); err != nil {
return
}
if err = this.generateTocNcx(); err != nil { //生成目录
return
}
if err = this.generateSummary(); err != nil { //生成文档内目录
return
}
if err = this.generateTitlePage(); err != nil { //生成封面
return
}
if err = this.generateContentOpf(); err != nil { //这个必须是generate*系列方法的最后一个调用
return
}
//将当前文件夹下的所有文件压缩成zip包然后直接改名成content.epub
f := this.BasePath + "/content.epub"
os.Remove(f) //如果原文件存在了,则删除;
if err = ziptil.Zip(f, this.BasePath); err == nil {
//创建导出文件夹
os.Mkdir(this.BasePath+"/"+output, os.ModePerm)
if len(this.Config.Format) > 0 {
var errs []string
for _, v := range this.Config.Format {
fmt.Println("convert to " + v)
switch strings.ToLower(v) {
case "epub":
if err = this.convertToEpub(); err != nil {
errs = append(errs, err.Error())
}
case "mobi":
if err = this.convertToMobi(); err != nil {
errs = append(errs, err.Error())
}
case "pdf":
if err = this.convertToPdf(); err != nil {
errs = append(errs, err.Error())
}
}
}
if len(errs) > 0 {
err = errors.New(strings.Join(errs, "\n"))
}
} else {
err = this.convertToPdf()
if err != nil {
fmt.Println(err)
}
}
}
return
}
//删除生成导出文档而创建的文件
func (this *Converter) converterDefer() {
//删除不必要的文件
os.RemoveAll(this.BasePath + "/META-INF")
os.RemoveAll(this.BasePath + "/content.epub")
os.RemoveAll(this.BasePath + "/mimetype")
os.RemoveAll(this.BasePath + "/toc.ncx")
os.RemoveAll(this.BasePath + "/content.opf")
os.RemoveAll(this.BasePath + "/titlepage.xhtml") //封面图片待优化
os.RemoveAll(this.BasePath + "/summary.html") //文档目录
}
//生成metainfo
func (this *Converter) generateMetaInfo() (err error) {
xml := `<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
`
folder := this.BasePath + "/META-INF"
os.MkdirAll(folder, os.ModePerm)
err = ioutil.WriteFile(folder+"/container.xml", []byte(xml), os.ModePerm)
return
}
//形成mimetyppe
func (this *Converter) generateMimeType() (err error) {
return ioutil.WriteFile(this.BasePath+"/mimetype", []byte("application/epub+zip"), os.ModePerm)
}
//生成封面
func (this *Converter) generateTitlePage() (err error) {
if ext := strings.ToLower(filepath.Ext(this.Config.Cover)); !(ext == ".html" || ext == ".xhtml") {
xml := `<?xml version='1.0' encoding='` + this.Config.Charset + `'?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="` + this.Config.Language + `">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=` + this.Config.Charset + `"/>
<meta name="calibre:cover" content="true"/>
<title>Cover</title>
<style type="text/css" title="override_css">
@page {padding: 0pt; margin:0pt}
body { text-align: center; padding:0pt; margin: 0pt; }
</style>
</head>
<body>
<div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="100%" height="100%" viewBox="0 0 800 1068" preserveAspectRatio="none">
<image width="800" height="1068" xlink:href="` + strings.TrimPrefix(this.Config.Cover, "./") + `"/>
</svg>
</div>
</body>
</html>
`
if err = ioutil.WriteFile(this.BasePath+"/titlepage.xhtml", []byte(xml), os.ModePerm); err == nil {
this.GeneratedCover = "titlepage.xhtml"
}
}
return
}
//生成文档目录
func (this *Converter) generateTocNcx() (err error) {
ncx := `<?xml version='1.0' encoding='` + this.Config.Charset + `'?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="%v">
<head>
<meta content="4" name="dtb:depth"/>
<meta content="calibre (2.85.1)" name="dtb:generator"/>
<meta content="0" name="dtb:totalPageCount"/>
<meta content="0" name="dtb:maxPageNumber"/>
</head>
<docTitle>
<text>%v</text>
</docTitle>
<navMap>%v</navMap>
</ncx>
`
codes, _ := this.tocToXml(0, 1)
ncx = fmt.Sprintf(ncx, this.Config.Language, this.Config.Title, strings.Join(codes, ""))
return ioutil.WriteFile(this.BasePath+"/toc.ncx", []byte(ncx), os.ModePerm)
}
//生成文档目录即summary.html
func (this *Converter) generateSummary() (err error) {
//目录
summary := `<!DOCTYPE html>
<html lang="` + this.Config.Language + `">
<head>
<meta charset="` + this.Config.Charset + `">
<title></title>
<style>
body{margin: 0px;padding: 0px;}h1{text-align: center;padding: 0px;margin: 0px;}ul,li{list-style: none;}
a{text-decoration: none;color: #4183c4;text-decoration: none;font-size: 16px;line-height: 28px;}
</style>
</head>
<body>
<h1>&nbsp;&nbsp;&nbsp;&nbsp;</h1>
%v
</body>
</html>`
summary = fmt.Sprintf(summary, strings.Join(this.tocToSummary(0), ""))
return ioutil.WriteFile(this.BasePath+"/summary.html", []byte(summary), os.ModePerm)
}
//将toc转成toc.ncx文件
func (this *Converter) tocToXml(pid, idx int) (codes []string, next_idx int) {
var code string
for _, toc := range this.Config.Toc {
if toc.Pid == pid {
code, idx = this.getNavPoint(toc, idx)
codes = append(codes, code)
for _, item := range this.Config.Toc {
if item.Pid == toc.Id {
code, idx = this.getNavPoint(item, idx)
codes = append(codes, code)
var code_arr []string
code_arr, idx = this.tocToXml(item.Id, idx)
codes = append(codes, code_arr...)
codes = append(codes, `</navPoint>`)
}
}
codes = append(codes, `</navPoint>`)
}
}
next_idx = idx
return
}
//将toc转成toc.ncx文件
func (this *Converter) tocToSummary(pid int) (summarys []string) {
summarys = append(summarys, "<ul>")
for _, toc := range this.Config.Toc {
if toc.Pid == pid {
summarys = append(summarys, fmt.Sprintf(`<li><a href="%v">%v</a></li>`, toc.Link, toc.Title))
for _, item := range this.Config.Toc {
if item.Pid == toc.Id {
summarys = append(summarys, fmt.Sprintf(`<li><ul><li><a href="%v">%v</a></li>`, item.Link, item.Title))
summarys = append(summarys, "<li>")
summarys = append(summarys, this.tocToSummary(item.Id)...)
summarys = append(summarys, "</li></ul></li>")
}
}
}
}
summarys = append(summarys, "</ul>")
return
}
//生成navPoint
func (this *Converter) getNavPoint(toc Toc, idx int) (navpoint string, nextidx int) {
navpoint = `
<navPoint id="id%v" playOrder="%v">
<navLabel>
<text>%v</text>
</navLabel>
<content src="%v"/>`
navpoint = fmt.Sprintf(navpoint, toc.Id, idx, toc.Title, toc.Link)
this.Config.Order = append(this.Config.Order, toc.Link)
nextidx = idx + 1
return
}
//生成content.opf文件
//倒数第二步调用
func (this *Converter) generateContentOpf() (err error) {
var (
guide string
manifest string
manifestArr []string
spine string //注意:如果存在封面,则需要把封面放在第一个位置
spineArr []string
)
meta := `<dc:title>%v</dc:title>
<dc:contributor opf:role="bkp">%v</dc:contributor>
<dc:publisher>%v</dc:publisher>
<dc:description>%v</dc:description>
<dc:language>%v</dc:language>
<dc:creator opf:file-as="Unknown" opf:role="aut">%v</dc:creator>
<meta name="calibre:timestamp" content="%v"/>
`
meta = fmt.Sprintf(meta, this.Config.Title, this.Config.Contributor, this.Config.Publisher, this.Config.Description, this.Config.Language, this.Config.Creator, this.Config.Timestamp)
if len(this.Config.Cover) > 0 {
meta = meta + `<meta name="cover" content="cover"/>`
guide = `<reference href="titlepage.xhtml" title="Cover" type="cover"/>`
manifest = fmt.Sprintf(`<item href="%v" id="cover" media-type="%v"/>`, this.Config.Cover, GetMediaType(filepath.Ext(this.Config.Cover)))
spineArr = append(spineArr, `<itemref idref="titlepage"/>`)
}
if _, err := os.Stat(this.BasePath + "/summary.html"); err == nil {
spineArr = append(spineArr, `<itemref idref="summary"/>`) //目录
}
//扫描所有文件
if files, err := filetil.ScanFiles(this.BasePath); err == nil {
basePath := strings.Replace(this.BasePath, "\\", "/", -1)
for _, file := range files {
if !file.IsDir {
ext := strings.ToLower(filepath.Ext(file.Path))
sourcefile := strings.TrimPrefix(file.Path, basePath+"/")
id := "ncx"
if ext != ".ncx" {
if file.Name == "titlepage.xhtml" { //封面
id = "titlepage"
} else if file.Name == "summary.html" { //目录
id = "summary"
} else {
id = cryptil.Md5Crypt(sourcefile)
}
}
if mt := GetMediaType(ext); mt != "" { //不是封面图片且media-type不为空
if sourcefile != strings.TrimLeft(this.Config.Cover, "./") { //不是封面图片,则追加进来。封面图片前面已经追加进来了
manifestArr = append(manifestArr, fmt.Sprintf(`<item href="%v" id="%v" media-type="%v"/>`, sourcefile, id, mt))
}
}
}
}
items := make(map[string]string)
for _, link := range this.Config.Order {
id := cryptil.Md5Crypt(link)
if _, ok := items[id]; !ok { //去重
items[id] = id
spineArr = append(spineArr, fmt.Sprintf(`<itemref idref="%v"/>`, id))
}
}
manifest = manifest + strings.Join(manifestArr, "\n")
spine = strings.Join(spineArr, "\n")
} else {
return err
}
pkg := `<?xml version='1.0' encoding='` + this.Config.Charset + `'?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0">
<metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata">
%v
</metadata>
<manifest>
%v
</manifest>
<spine toc="ncx">
%v
</spine>
%v
</package>
`
if len(guide) > 0 {
guide = `<guide>` + guide + `</guide>`
}
pkg = fmt.Sprintf(pkg, meta, manifest, spine, guide)
return ioutil.WriteFile(this.BasePath+"/content.opf", []byte(pkg), os.ModePerm)
}
//转成epub
func (this *Converter) convertToEpub() (err error) {
args := []string{
this.BasePath + "/content.epub",
this.BasePath + "/" + output + "/book.epub",
}
cmd := exec.Command(ebookConvert, args...)
if this.Debug {
fmt.Println(cmd.Args)
}
return cmd.Run()
}
//转成mobi
func (this *Converter) convertToMobi() (err error) {
args := []string{
this.BasePath + "/content.epub",
this.BasePath + "/" + output + "/book.mobi",
}
cmd := exec.Command(ebookConvert, args...)
if this.Debug {
fmt.Println(cmd.Args)
}
return cmd.Run()
}
//转成pdf
func (this *Converter) convertToPdf() (err error) {
args := []string{
this.BasePath + "/content.epub",
this.BasePath + "/" + output + "/book.pdf",
}
//页面大小
if len(this.Config.PaperSize) > 0 {
args = append(args, "--paper-size", this.Config.PaperSize)
}
//文字大小
if len(this.Config.FontSize) > 0 {
args = append(args, "--pdf-default-font-size", this.Config.FontSize)
}
//header template
if len(this.Config.Header) > 0 {
args = append(args, "--pdf-header-template", this.Config.Header)
}
//footer template
if len(this.Config.Footer) > 0 {
args = append(args, "--pdf-footer-template", this.Config.Footer)
}
if len(this.Config.MarginLeft) > 0 {
args = append(args, "--pdf-page-margin-left", this.Config.MarginLeft)
}
if len(this.Config.MarginTop) > 0 {
args = append(args, "--pdf-page-margin-top", this.Config.MarginTop)
}
if len(this.Config.MarginRight) > 0 {
args = append(args, "--pdf-page-margin-right", this.Config.MarginRight)
}
if len(this.Config.MarginBottom) > 0 {
args = append(args, "--pdf-page-margin-bottom", this.Config.MarginBottom)
}
//更多选项
if len(this.Config.More) > 0 {
args = append(args, this.Config.More...)
}
cmd := exec.Command(ebookConvert, args...)
if this.Debug {
fmt.Println(cmd.Args)
}
return cmd.Run()
}

47
converter/util.go 100644
View File

@ -0,0 +1,47 @@
//Author:TruthHun
//Email:TruthHun@QQ.COM
//Date:2018-01-21
package converter
import (
"encoding/json"
"io/ioutil"
"strings"
)
//media-type
var MediaType = map[string]string{
".jpeg": "image/jpeg",
".png": "image/png",
".jpg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
".bmp": "image/bmp",
".html": "application/xhtml+xml",
".xhtml": "application/xhtml+xml",
".htm": "application/xhtml+xml",
".otf": "application/x-font-opentype",
".ttf": "application/x-font-ttf",
".js": "application/x-javascript",
".ncx": "x-dtbncx+xml",
".txt": "text/plain",
".xml": "text/xml",
".css": "text/css",
}
//根据文件扩展名获取media-type
func GetMediaType(ext string) string {
if mt, ok := MediaType[strings.ToLower(ext)]; ok {
return mt
}
return ""
}
//解析配置文件
func parseConfig(configFile string) (cfg Config, err error) {
var b []byte
if b, err = ioutil.ReadFile(configFile); err == nil {
err = json.Unmarshal(b, &cfg)
}
return
}

View File

@ -3,8 +3,6 @@ package models
import (
"time"
"strings"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
"github.com/astaxie/beego/orm"
@ -24,6 +22,10 @@ type Book struct {
OrderIndex int `orm:"column(order_index);type(int);default(0)" json:"order_index"`
// Description 项目描述.
Description string `orm:"column(description);size(2000)" json:"description"`
//发行公司
Publisher string `orm:"column(publisher);size(500)" json:"publisher"`
//是否缓存导出的电子书,如果缓存可能会出现导出的文件不是最新的。 0 为不缓存
IsCacheEBook int `orm:"column(is_cache_ebook);type(int);default(0)" json:"is_cache_ebook"`
Label string `orm:"column(label);size(500)" json:"label"`
// PrivatelyOwned 项目私有: 0 公开/ 1 私有
PrivatelyOwned int `orm:"column(privately_owned);type(int);default(0)" json:"privately_owned"`
@ -354,38 +356,6 @@ func (m *Book) FindForLabelToPager(keyword string, pageIndex, pageSize, member_i
}
func (book *Book) ToBookResult() *BookResult {
m := NewBookResult()
m.BookId = book.BookId
m.BookName = book.BookName
m.Identify = book.Identify
m.OrderIndex = book.OrderIndex
m.Description = strings.Replace(book.Description, "\r\n", "<br/>", -1)
m.PrivatelyOwned = book.PrivatelyOwned
m.PrivateToken = book.PrivateToken
m.DocCount = book.DocCount
m.CommentStatus = book.CommentStatus
m.CommentCount = book.CommentCount
m.CreateTime = book.CreateTime
m.ModifyTime = book.ModifyTime
m.Cover = book.Cover
m.Label = book.Label
m.Status = book.Status
m.Editor = book.Editor
m.Theme = book.Theme
m.AutoRelease = book.AutoRelease == 1
if book.Theme == "" {
m.Theme = "default"
}
if book.Editor == "" {
m.Editor = "markdown"
}
return m
}
//重置文档数量
func (m *Book) ResetDocumentNumber(book_id int) {
o := orm.NewOrm()

View File

@ -2,10 +2,20 @@ package models
import (
"time"
"bytes"
"github.com/astaxie/beego/orm"
"github.com/astaxie/beego/logs"
"github.com/lifei6671/mindoc/conf"
"strings"
"github.com/lifei6671/mindoc/converter"
"strconv"
"github.com/russross/blackfriday"
"path/filepath"
"github.com/astaxie/beego"
"os"
"github.com/PuerkitoBio/goquery"
"github.com/lifei6671/mindoc/utils"
)
type BookResult struct {
@ -14,6 +24,8 @@ type BookResult struct {
Identify string `json:"identify"`
OrderIndex int `json:"order_index"`
Description string `json:"description"`
Publisher string `json:"publisher"`
IsCacheEBook bool `json:"is_cache_ebook"`
PrivatelyOwned int `json:"privately_owned"`
PrivateToken string `json:"private_token"`
DocCount int `json:"doc_count"`
@ -79,13 +91,14 @@ func (m *BookResult) FindByIdentify(identify string,member_id int) (*BookResult,
return m, err
}
m = book.ToBookResult()
m = NewBookResult().ToBookResult(*book)
m.CreateName = member.Account
m.MemberId = relationship.MemberId
m.RoleId = relationship.RoleId
m.RelationshipId = relationship.RelationshipId
if m.RoleId == conf.BookFounder {
m.RoleName = "创始人"
} else if m.RoleId == conf.BookAdmin {
@ -134,6 +147,183 @@ func (m *BookResult) FindToPager(pageIndex, pageSize int) (books []*BookResult,t
return
}
//实体转换
func (m *BookResult) ToBookResult(book Book) *BookResult {
m.BookId = book.BookId
m.BookName = book.BookName
m.Identify = book.Identify
m.OrderIndex = book.OrderIndex
m.Description = strings.Replace(book.Description, "\r\n", "<br/>", -1)
m.PrivatelyOwned = book.PrivatelyOwned
m.PrivateToken = book.PrivateToken
m.DocCount = book.DocCount
m.CommentStatus = book.CommentStatus
m.CommentCount = book.CommentCount
m.CreateTime = book.CreateTime
m.ModifyTime = book.ModifyTime
m.Cover = book.Cover
m.Label = book.Label
m.Status = book.Status
m.Editor = book.Editor
m.Theme = book.Theme
m.AutoRelease = book.AutoRelease == 1
m.Publisher = book.Publisher
m.IsCacheEBook = book.IsCacheEBook == 1
if book.Theme == "" {
m.Theme = "default"
}
if book.Editor == "" {
m.Editor = "markdown"
}
return m
}
func (m *BookResult) Converter(sessionId string) (ConvertBookResult,error) {
convertBookResult := ConvertBookResult{}
outputPath := filepath.Join(beego.AppConfig.DefaultString("book_output_path","cache"),sessionId,strconv.Itoa(m.BookId))
if m.IsCacheEBook {
outputPath = filepath.Join(beego.AppConfig.DefaultString("book_output_path","cache"),strconv.Itoa(m.BookId))
}
if m.IsCacheEBook {
pdfpath := filepath.Join(outputPath,"output","book.pdf")
epubpath := filepath.Join(outputPath,"output","book.epub")
mobipath := filepath.Join(outputPath,"output","book.mobi")
if utils.FileExists(pdfpath) && utils.FileExists(epubpath) && utils.FileExists(mobipath){
convertBookResult.EpubPath = epubpath
convertBookResult.MobiPath = mobipath
convertBookResult.PDFPath = pdfpath
return convertBookResult,nil
}
}
docs, err := NewDocument().FindListByBookId(m.BookId)
if err != nil {
return convertBookResult,err
}
tocList := make([]converter.Toc,0)
for _, item := range docs {
if item.ParentId == 0 {
toc := converter.Toc{
Id: item.DocumentId,
Link: strconv.Itoa(item.DocumentId) + ".html",
Pid: item.ParentId,
Title: item.DocumentName,
}
tocList = append(tocList,toc)
}
}
for _, item := range docs {
if item.ParentId != 0 {
toc := converter.Toc{
Id: item.DocumentId,
Link: strconv.Itoa(item.DocumentId) + ".html",
Pid: item.ParentId,
Title: item.DocumentName,
}
tocList = append(tocList,toc)
}
}
ebookConfig := converter.Config{
Charset : "utf-8",
Cover : m.Cover,
Timestamp : time.Now().Format("2006-01-02 15:04:05"),
Description : string(blackfriday.MarkdownBasic([]byte(m.Description))),
Footer : "<p style='color:#8E8E8E;font-size:12px;'>本文档使用 <a href='https://www.iminho.me' style='text-decoration:none;color:#1abc9c;font-weight:bold;'>MinDoc</a> 构建 <span style='float:right'>- _PAGENUM_ -</span></p>",
Header : "<p style='color:#8E8E8E;font-size:12px;'>_SECTION_</p>",
Identifier : "",
Language : "zh-CN",
Creator : m.CreateName,
Publisher : m.Publisher,
Contributor : m.Publisher,
Title : m.BookName,
Format: []string{"epub", "mobi", "pdf"},
FontSize : "14",
PaperSize : "a4",
MarginLeft : "72",
MarginRight : "72",
MarginTop : "72",
MarginBottom : "72",
Toc : tocList,
More : []string{},
}
os.MkdirAll(outputPath, 0766)
if outputPath, err = filepath.Abs(outputPath); err != nil {
beego.Error("导出目录配置错误:" + err.Error())
return convertBookResult,err
}
viewPath := beego.BConfig.WebConfig.ViewsPath
baseUrl := beego.AppConfig.DefaultString("baseurl","")
for _,item := range docs {
name := strconv.Itoa(item.DocumentId)
fpath := filepath.Join(outputPath,name + ".html")
f, err := os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0777)
if err != nil {
return convertBookResult,err
}
var buf bytes.Buffer
if err := beego.ExecuteViewPathTemplate(&buf,"document/export.tpl",viewPath,map[string]interface{}{"Model": m, "Lists": item, "BaseUrl": baseUrl}); err != nil {
return convertBookResult,err
}
html := buf.String()
if err != nil {
f.Close()
return convertBookResult,err
}
bufio := bytes.NewReader(buf.Bytes())
doc, err := goquery.NewDocumentFromReader(bufio)
doc.Find("img").Each(func(i int, contentSelection *goquery.Selection) {
if src, ok := contentSelection.Attr("src"); ok && strings.HasPrefix(src, "/uploads/") {
contentSelection.SetAttr("src", baseUrl + src)
}
})
html, err = doc.Html()
if err != nil {
f.Close()
return convertBookResult,err
}
// html = strings.Replace(html, "<img src=\"/uploads", "<img src=\"" + c.BaseUrl() + "/uploads", -1)
f.WriteString(html)
f.Close()
}
eBookConverter := &converter.Converter{
BasePath : outputPath,
Config : ebookConfig,
Debug : false,
}
if err := eBookConverter.Convert();err != nil {
beego.Error("转换文件错误:" + m.BookName +" => "+ err.Error())
return convertBookResult,err
}
convertBookResult.MobiPath = filepath.Join(outputPath,"output","book.mobi")
convertBookResult.PDFPath = filepath.Join(outputPath,"output","book.pdf")
convertBookResult.EpubPath = filepath.Join(outputPath,"output","book.epub")
return convertBookResult,nil
}

View File

@ -0,0 +1,8 @@
package models
// 转换结果
type ConvertBookResult struct {
PDFPath string
EpubPath string
MobiPath string
}

View File

@ -8,6 +8,7 @@ import (
"github.com/astaxie/beego"
"github.com/astaxie/beego/orm"
"github.com/lifei6671/mindoc/conf"
"strings"
)
// Document struct.
@ -137,6 +138,9 @@ func (m *Document) ReleaseContent(book_id int) {
if err == nil && len(attach_list) > 0 {
content := bytes.NewBufferString("<div class=\"attach-list\"><strong>附件</strong><ul>")
for _, attach := range attach_list {
if strings.HasPrefix(attach.HttpPath,"/"){
attach.HttpPath = strings.TrimSuffix(beego.AppConfig.DefaultString("baseurl",""),"/") + attach.HttpPath
}
li := fmt.Sprintf("<li><a href=\"%s\" target=\"_blank\" title=\"%s\">%s</a></li>", attach.HttpPath, attach.FileName, attach.FileName)
content.WriteString(li)

View File

@ -72,8 +72,7 @@ func init() {
beego.Router("/docs/:key", &controllers.DocumentController{}, "*:Index")
beego.Router("/docs/:key/:id", &controllers.DocumentController{}, "*:Read")
beego.Router("/docs/:key/search", &controllers.DocumentController{}, "post:Search")
beego.Router("/export/:key", &controllers.DocumentController{}, "*:ExportBook")
beego.Router("/export/:key/:id", &controllers.DocumentController{}, "*:ExportDoc")
beego.Router("/export/:key", &controllers.DocumentController{}, "*:Export")
beego.Router("/qrcode/:key.png", &controllers.DocumentController{}, "get:QrCode")
beego.Router("/attach_files/:key/:attach_id", &controllers.DocumentController{}, "get:DownloadAttachment")

View File

@ -88,7 +88,6 @@ function openDeleteDocumentDialog($node) {
layer.close(index);
if(res.errcode === 0){
window.treeCatalog.delete_node($node);
resetEditor($node);
}else{
layer.msg("删除失败",{icon : 2})
}
@ -153,6 +152,24 @@ function pushVueLists($lists) {
}
}
/**
*
*/
function releaseBook() {
$.ajax({
url: window.releaseURL,
data: { "identify": window.book.identify },
type: "post",
dataType: "json",
success: function (res) {
if (res.errcode === 0) {
layer.msg("发布任务已推送到任务队列,稍后将在后台执行。");
} else {
layer.msg(res.message);
}
}
});
}
//实现小提示
$("[data-toggle='tooltip']").hover(function () {
var title = $(this).attr('data-title');
@ -238,7 +255,6 @@ function uploadImage($id,$callback) {
var imageFile = clipboard.items[i].getAsFile();
console.log(imageFile)
var fileName = Date.parse(new Date());
switch (imageFile.type){

View File

@ -228,24 +228,6 @@ $(function () {
});
}
function releaseBook() {
$.ajax({
url: window.releaseURL,
data: { "identify": window.book.identify },
type: "post",
dataType: "json",
success: function (res) {
if (res.errcode === 0) {
layer.msg("发布任务已推送到任务队列,稍后将在后台执行。");
} else {
layer.msg(res.message);
}
}
});
}
function resetEditor($node) {
}
/**
*

View File

@ -8,14 +8,83 @@ $(function () {
toolbar :"#editormd-tools"
}
});
window.editor.on("editor-change",function () {
window.editor.on("text-change",function () {
resetEditorChanged(true);
});
window.menu_save.on("click",function () {
if($(this).hasClass('change')){
saveDocument();
var $editorEle = $("#editormd-tools");
$editorEle.find(".ql-undo").on("click",function () {
window.editor.history.undo();
});
$editorEle.find(".ql-redo").on("click",function () {
window.editor.history.redo();
});
$("#btnRelease").on("click",function () {
if (Object.prototype.toString.call(window.documentCategory) === '[object Array]' && window.documentCategory.length > 0) {
if ($("#markdown-save").hasClass('change')) {
var comfirm_result = confirm("编辑内容未保存,需要保存吗?")
if (comfirm_result) {
saveDocument(false, releaseBook);
return;
}
}
releaseBook();
} else {
layer.msg("没有需要发布的文档")
}
});
/**
*
*/
window.editor.getModule('toolbar').addHandler('image',function () {
var input = document.createElement('input');
input.setAttribute('type', 'file');
input.click();
// Listen upload local image and save to server
input.onchange = function () {
var file = input.files[0];
// file type is only image.
if (/^image\//.test(file.type)) {
var form = new FormData();
form.append('editormd-image-file', file, file.name);
var layerIndex = 0;
$.ajax({
url: window.imageUploadURL,
type: "POST",
dataType: "json",
data: form,
processData: false,
contentType: false,
error: function() {
layer.close(layerIndex);
layer.msg("图片上传失败");
},
success: function(data) {
layer.close(layerIndex);
if(data.errcode !== 0){
layer.msg(data.message);
}else{
var range = window.editor.getSelection();
editor.insertEmbed(range.index, 'image', data.url);
}
}
});
} else {
console.warn('You could only upload images.');
}
};
});
/**
*
*/
window.menu_save.on("click",function () {if($(this).hasClass('change')){saveDocument();}});
/**
*
* @param $is_change
@ -43,13 +112,14 @@ $(function () {
if(res.errcode === 0){
window.isLoad = true;
window.editor.setContents([{ insert: res.data.content }]);
window.editor.root.innerHTML = res.data.content;
// 将原始内容备份
window.source = res.data.content;
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;
window.isLoad = true;
pushVueLists(res.data.attach);
@ -70,8 +140,9 @@ $(function () {
var index = null;
var node = window.selectNode;
var html = window.editor.getContents();
var html = window.editor.root.innerHTML;
console.log(html)
var content = "";
if($.trim(html) !== ""){
content = toMarkdown(html, { gfm: true });
@ -111,10 +182,9 @@ $(function () {
break;
}
}
resetEditorChanged(false);
// 更新内容备份
window.source = res.data.content;
// 触发编辑器 onchange 回调函数
window.editor.onchange();
if(typeof callback === "function"){
callback();
}
@ -231,8 +301,20 @@ $(function () {
}
}).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);
}
}
}).on('select_node.jstree', function (node, selected, event) {
if(window.menu_save.hasClass('selected')) {
if(window.menu_save.hasClass('change')) {
if(confirm("编辑内容未保存,需要保存吗?")){
saveDocument(false,function () {
loadDocument(selected);
@ -242,7 +324,11 @@ $(function () {
}
loadDocument(selected);
}).on("move_node.jstree", jstree_save);
}).on("move_node.jstree", jstree_save)
.on("delete_node.jstree",function (node,parent) {
window.isLoad = true;
window.editor.root.innerHTML ='';
});
window.saveDocument = saveDocument;

View File

@ -1,7 +1,7 @@
(function () {
var icons = Quill.import('ui/icons');
icons.header[3] = '<svg viewBox="0 0 18 18">\n' +
' <path class="ql-fill" d="M14.51758,9.64453a1.85627,1.85627,0,0,0-1.24316.38477H13.252a1.73532,1.73532,0,0,1,1.72754-1.4082,2.66491,2.66491,0,0,1,.5498.06641c.35254.05469.57227.01074.70508-.40723l.16406-.5166a.53393.53393,0,0,0-.373-.75977,4.83723,4.83723,0,0,0-1.17773-.14258c-2.43164,0-3.7627,2.17773-3.7627,4.43359,0,2.47559,1.60645,3.69629,3.19043,3.69629A2.70585,2.70585,0,0,0,16.96,12.19727,2.43861,2.43861,0,0,0,14.51758,9.64453Zm-.23047,3.58691c-.67187,0-1.22168-.81445-1.22168-1.45215,0-.47363.30762-.583.72559-.583.96875,0,1.27734.59375,1.27734,1.12207A.82182.82182,0,0,1,14.28711,13.23145ZM10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Z"/>\n' +
' <path class="ql-fill" d="M16.65186,12.30664a2.6742,2.6742,0,0,1-2.915,2.68457,3.96592,3.96592,0,0,1-2.25537-.6709.56007.56007,0,0,1-.13232-.83594L11.64648,13c.209-.34082.48389-.36328.82471-.1543a2.32654,2.32654,0,0,0,1.12256.33008c.71484,0,1.12207-.35156,1.12207-.78125,0-.61523-.61621-.86816-1.46338-.86816H13.2085a.65159.65159,0,0,1-.68213-.41895l-.05518-.10937a.67114.67114,0,0,1,.14307-.78125l.71533-.86914a8.55289,8.55289,0,0,1,.68213-.7373V8.58887a3.93913,3.93913,0,0,1-.748.05469H11.9873a.54085.54085,0,0,1-.605-.60547V7.59863a.54085.54085,0,0,1,.605-.60547h3.75146a.53773.53773,0,0,1,.60547.59375v.17676a1.03723,1.03723,0,0,1-.27539.748L14.74854,10.0293A2.31132,2.31132,0,0,1,16.65186,12.30664ZM9,3A.99974.99974,0,0,0,8,4V8H3V4A1,1,0,0,0,1,4V14a1,1,0,0,0,2,0V10H8v4a1,1,0,0,0,2,0V4A.99974.99974,0,0,0,9,3Z"/>\n' +
'</svg>';
icons.header[4] = '<svg viewBox="0 0 18 18">\n' +
' <path class="ql-fill" d="M10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Zm7.05371,7.96582v.38477c0,.39648-.165.60547-.46191.60547h-.47314v1.29785a.54085.54085,0,0,1-.605.60547h-.69336a.54085.54085,0,0,1-.605-.60547V12.95605H11.333a.5412.5412,0,0,1-.60547-.60547v-.15332a1.199,1.199,0,0,1,.22021-.748l2.56348-4.05957a.7819.7819,0,0,1,.72607-.39648h1.27637a.54085.54085,0,0,1,.605.60547v3.7627h.33008A.54055.54055,0,0,1,17.05371,11.96582ZM14.28125,8.7207h-.022a4.18969,4.18969,0,0,1-.38525.81348l-1.188,1.80469v.02246h1.5293V9.60059A7.04058,7.04058,0,0,1,14.28125,8.7207Z"/>\n' +

View File

@ -40,31 +40,34 @@
<span style="font-size: 12px;font-weight: 100;"></span>
</div>
<div class="navbar-header pull-right manual-menu">
<div class="dropdown">
<button id="dLabel" class="btn btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
项目
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="dLabel">
{{if gt .Member.MemberId 0}}
{{if gt .Model.RelationshipId 0}}
{{if eq .Model.RoleId 0 1 2}}
<li><a href="{{urlfor "DocumentController.Edit" ":key" .Model.Identify ":id" ""}}">返回编辑</a> </li>
<div class="dropdown pull-right">
<a href="{{urlfor "DocumentController.Edit" ":key" .Model.Identify ":id" ""}}" class="btn btn-default">编辑</a>
</div>
{{end}}
{{end}}
<li><a href="{{urlfor "BookController.Index"}}">我的项目</a> </li>
<li role="presentation" class="divider"></li>
{{end}}
<div class="dropdown pull-right" style="margin-right: 10px;">
<a href="{{urlfor "HomeController.Index"}}" class="btn btn-default"><i class="fa fa-home" aria-hidden="true"></i> 首页</a>
</div>
<div class="dropdown pull-right" style="margin-right: 10px;">
{{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="javascript:void(0);" onclick="ExportPdfDoc()">文档导出为 PDF</a> </li>
<li><a href="{{urlfor "DocumentController.ExportBook" ":key" .Model.Identify "output" "pdf"}}" target="_blank">项目导出为 PDF</a> </li>
<button type="button" class="btn btn-success" data-toggle="modal" data-target="#shareProject"><i class="fa fa-share-alt" aria-hidden="true"></i> 分享</button>
{{end}}
<li><a href="{{urlfor "HomeController.Index"}}" title="返回首页">返回首页</a> </li>
</div>
<div class="dropdown pull-right" style="margin-right: 10px;">
<button type="button" class="btn btn-primary" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
下载 <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel" style="margin-top: -5px;">
<li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "pdf"}}" target="_blank">PDF</a> </li>
<li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "epub"}}" target="_blank">EPUB</a> </li>
<li><a href="{{urlfor "DocumentController.Export" ":key" .Model.Identify "output" "mobi"}}" target="_blank">MOBI</a> </li>
</ul>
</div>
</div>
</div>
</header>
@ -128,7 +131,7 @@
</div>
<div class="col-md-8 text-center">
<h1 id="article-title">{{.Title}}</h1>
<h3 id="article-info" class="article-info">{{.Info}}</h3>
{{/*<h3 id="article-info" class="article-info">{{.Info}}</h3>*/}}
</div>
<div class="col-md-2">
</div>
@ -198,7 +201,7 @@
<div class="manual-mask"></div>
</div>
<!-- Share Modal -->
<!-- 分享项目 -->
<div class="modal fade" id="shareProject" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
@ -226,10 +229,39 @@
</div>
</div>
</div>
<!-- 下载项目 -->
<div class="modal fade" id="downloadBookModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
<h4 class="modal-title" id="myModalLabel">项目分享</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-sm-12 text-center" style="padding-bottom: 15px;">
<img src="{{urlfor "DocumentController.QrCode" ":key" .Model.Identify}}" alt="扫一扫手机阅读" />
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-2 control-label">项目地址</label>
<div class="col-sm-10">
<input type="text" value="{{.BaseUrl}}{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" class="form-control" onmouseover="this.select()" id="projectUrl" title="项目地址">
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
<script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
<script src="{{cdnjs "/static/js/jquery.form.js"}}" type="text/javascript"></script>
<script src="/static/layer/layer.js" type="text/javascript"></script>
<script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/jstree/3.3.4/jstree.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/nprogress/nprogress.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/highlight/highlight.js"}}" type="text/javascript"></script>

View File

@ -48,7 +48,7 @@
border-left: none;
height: 100%;
outline:none;
padding: 5px;
padding: 5px 5px 30px 5px;
}
.btn-info{background-color: #ffffff !important;}
.btn-info>i{background-color: #cacbcd !important; color: #393939 !important; box-shadow: inset 0 0 0 1px transparent,inset 0 0 0 0 rgba(34,36,38,.15);}
@ -182,12 +182,13 @@
<body>
<div class="m-manual manual-editor">
<div class="manual-head btn-toolbar" id="editormd-tools" style="min-width: 1600px;" data-role="editor-toolbar" data-target="#editor">
<div class="manual-head btn-toolbar" id="editormd-tools" style="min-width: 1360px;" data-role="editor-toolbar" data-target="#editor">
<div class="editor-group">
<a href="{{urlfor "BookController.Index"}}" data-toggle="tooltip" data-title="返回"><i class="fa fa-chevron-left" aria-hidden="true"></i></a>
</div>
<div class="editor-group">
<a href="javascript:;" id="markdown-save" data-toggle="tooltip" data-title="保存" class="disabled save"><i class="fa fa-save" aria-hidden="true" name="save"></i></a>
<a href="javascript:;" id="markdown-save" data-toggle="tooltip" data-title="保存" class="disabled save"><i class="fa fa-save first" aria-hidden="true" name="save"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="发布" id="btnRelease"><i class="fa fa-cloud-upload last" name="release" aria-hidden="true"></i></a>
</div>
<div class="editor-group">
<a href="javascript:;" data-toggle="tooltip" data-title="撤销 (Ctrl-Z)" class="ql-undo"><i class="fa fa-undo first" name="undo" unselectable="on"></i></a>
@ -231,30 +232,13 @@
<button data-toggle="tooltip" data-title="公式" class="ql-formula editor-item"><i class="fa fa-tasks item" name="tasks" aria-hidden="true"></i></button>
<select data-toggle="tooltip" data-title="字体颜色" class="ql-color ql-picker ql-color-picker editor-item-select" ></select>
<select data-toggle="tooltip" data-title="背景颜色" class="ql-background editor-item-select"></select>
<a href="javascript:;" data-toggle="tooltip" data-title="附件"><i class="fa fa-paperclip item" aria-hidden="true" name="attachment"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="模板"><i class="fa fa-tachometer last" name="template"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="附件" id="btnUploadFile"><i class="fa fa-paperclip last" aria-hidden="true" name="attachment"></i></a>
</div>
<div class="editormd-group pull-right">
<a href="javascript:;" data-toggle="tooltip" data-title="关闭实时预览"><i class="fa fa-eye-slash first" name="watch" unselectable="on"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="修改历史"><i class="fa fa-history item" name="history" aria-hidden="true"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="边栏"><i class="fa fa-columns item" aria-hidden="true" name="sidebar"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="使用帮助"><i class="fa fa-question-circle-o last" aria-hidden="true" name="help"></i></a>
</div>
<div class="editormd-group pull-right">
<a href="javascript:;" data-toggle="tooltip" data-title="发布"><i class="fa fa-cloud-upload" name="release" aria-hidden="true"></i></a>
</div>
<div class="editor-group">
<a href="javascript:;" data-toggle="tooltip" data-title=""></a>
<a href="javascript:;" data-toggle="tooltip" data-title=""></a>
</div>
<div class="clearfix"></div>
</div>
<div class="manual-body" style="min-width: 1600px;right: inherit">
<div class="manual-body">
<div class="manual-category" id="manualCategory" style=" border-right: 1px solid #DDDDDD;width: 281px;position: absolute;">
<div class="manual-nav">
<div class="nav-item active"><i class="fa fa-bars" aria-hidden="true"></i> 文档</div>
@ -263,26 +247,14 @@
</div>
<div class="manual-tree" id="sidebar"> </div>
</div>
<div class="manual-editor-container" id="manualEditorContainer" style="min-width: 1319px;">
<div class="manual-editormd">
<div id="docEditor" class="manual-editormd-active ql-editor ql-blank">
MinDoc 是一款针对IT团队开发的简单好用的文档管理系统。
MinDoc 的前身是 SmartWiki 文档系统。SmartWiki 是基于 PHP 框架 laravel 开发的一款文档管理系统。因 PHP 的部署对普通用户来说太复杂,所以改用 Golang 开发。可以方便用户部署和实用。
开发缘起是公司IT部门需要一款简单实用的项目接口文档管理和分享的系统。其功能和界面源于 kancloud 。
可以用来储存日常接口文档,数据库字典,手册说明等文档。内置项目管理,用户管理,权限管理等功能,能够满足大部分中小团队的文档管理需求。
<div contenteditable="false" class="editor-wrapper"><pre><code class="editor-code">f</code></pre></div>
<div><br/></div>
</div>
</div>
<div class="manual-editor-status">
<div class="manual-editor-container" id="manualEditorContainer" style="min-width: 1060px;">
<div class="manual-editormd" style="bottom: 0;">
<div id="docEditor" class="manual-editormd-active ql-editor ql-blank"></div>
<div class="manual-editor-status" style="border-top: 1px solid #DDDDDD;">
<div id="attachInfo" class="item">0 个附件</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加文档 -->
@ -334,11 +306,9 @@
<div class="modal-body">
<div class="attach-drop-panel">
<div class="upload-container" id="filePicker">
<div class="webuploader-pick">
<i class="fa fa-upload" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="attach-list" id="attachList">
<template v-for="item in lists">
<div class="attach-item" :id="item.attachment_id">
@ -395,57 +365,6 @@
</div>
</div>
<div class="modal fade" id="documentTemplateModal" tabindex="-1" role="dialog" aria-labelledby="请选择模板类型" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="modal-title">请选择模板类型</h4>
</div>
<div class="modal-body template-list">
<div class="container">
<div class="section">
<a data-type="normal" href="javascript:;"><i class="fa fa-file-o"></i></a>
<h3><a data-type="normal" href="javascript:;">普通文档</a></h3>
<ul>
<li>默认类型</li>
<li>简单的文本文档</li>
</ul>
</div>
<div class="section">
<a data-type="api" href="javascript:;"><i class="fa fa-file-code-o"></i></a>
<h3><a data-type="api" href="javascript:;">API文档</a></h3>
<ul>
<li>用于API文档速写</li>
<li>支持代码高亮</li>
</ul>
</div>
<div class="section">
<a data-type="code" href="javascript:;"><i class="fa fa-book"></i></a>
<h3><a data-type="code" href="javascript:;">数据字典</a></h3>
<ul>
<li>用于数据字典显示</li>
<li>表格支持</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<template id="template-normal">
{{template "document/template_normal.tpl"}}
</template>
<template id="template-api">
{{template "document/template_api.tpl"}}
</template>
<template id="template-code">
{{template "document/template_code.tpl"}}
</template>
<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>
@ -455,6 +374,7 @@
{{/*<script src="/static/bootstrap/plugins/bootstrap-wysiwyg/bootstrap-wysiwyg.js" type="text/javascript"></script>*/}}
{{/*<script src="/static/bootstrap/plugins/bootstrap-wysiwyg/external/google-code-prettify/prettify.js"></script>*/}}
<script src="/static/katex/katex.min.js" type="text/javascript"></script>
<script src="/static/to-markdown/dist/to-markdown.js" type="text/javascript"></script>
<script src="/static/quill/quill.js" type="text/javascript"></script>
<script src="/static/quill/quill.icons.js"></script>
<script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript" ></script>
@ -465,14 +385,7 @@
$(function () {
var $editorEle = $("#editormd-tools");
$editorEle.find(".ql-undo").on("click",function () {
quill.history.undo();
});
$editorEle.find(".ql-redo").on("click",function () {
quill.history.redo();
});
$(".editor-code").on("dblclick",function () {
var code = $(this).html();
@ -485,7 +398,7 @@
$(this).parents(".editor-wrapper").addClass("editor-wrapper-selected");
});
$("#attachInfo").on("click",function () {
$("#attachInfo,#btnUploadFile").on("click",function () {
$("#uploadAttachModal").modal("show");
});