소스 검색

feat:实现项目集功能

lifei6671 7 년 전
부모
커밋
91df6bb024

+ 11 - 9
commands/command.go

@@ -105,6 +105,7 @@ func RegisterModel() {
 		new(models.Team),
 		new(models.TeamMember),
 		new(models.TeamRelationship),
+		new(models.Itemsets),
 	)
 	gob.Register(models.Blog{})
 	gob.Register(models.Document{})
@@ -449,32 +450,33 @@ func RegisterAutoLoadConfig() {
 		}
 	}
 }
+
 //注册错误处理方法.
-func RegisterError()  {
+func RegisterError() {
 	beego.ErrorHandler("404", func(writer http.ResponseWriter, request *http.Request) {
 		var buf bytes.Buffer
 
-		data :=make(map[string]interface{})
+		data := make(map[string]interface{})
 		data["ErrorCode"] = 404
 		data["ErrorMessage"] = "页面未找到或已删除"
 
-		if err := beego.ExecuteViewPathTemplate(&buf,"errors/error.tpl",beego.BConfig.WebConfig.ViewsPath,data);err == nil {
-			fmt.Fprint(writer,buf.String())
+		if err := beego.ExecuteViewPathTemplate(&buf, "errors/error.tpl", beego.BConfig.WebConfig.ViewsPath, data); err == nil {
+			fmt.Fprint(writer, buf.String())
 		} else {
-			fmt.Fprint(writer,data["ErrorMessage"])
+			fmt.Fprint(writer, data["ErrorMessage"])
 		}
 	})
 	beego.ErrorHandler("401", func(writer http.ResponseWriter, request *http.Request) {
 		var buf bytes.Buffer
 
-		data :=make(map[string]interface{})
+		data := make(map[string]interface{})
 		data["ErrorCode"] = 401
 		data["ErrorMessage"] = "请与 Web 服务器的管理员联系,以确认您是否具有访问所请求资源的权限。"
 
-		if err := beego.ExecuteViewPathTemplate(&buf,"errors/error.tpl",beego.BConfig.WebConfig.ViewsPath,data);err == nil {
-			fmt.Fprint(writer,buf.String())
+		if err := beego.ExecuteViewPathTemplate(&buf, "errors/error.tpl", beego.BConfig.WebConfig.ViewsPath, data); err == nil {
+			fmt.Fprint(writer, buf.String())
 		} else {
-			fmt.Fprint(writer,data["ErrorMessage"])
+			fmt.Fprint(writer, data["ErrorMessage"])
 		}
 	})
 }

+ 12 - 2
commands/install.go

@@ -135,8 +135,18 @@ func initialization() {
 		book.Theme = "default"
 
 		if err := book.Insert(); err != nil {
-			panic("Book.Insert => " + err.Error())
-			os.Exit(0)
+			panic("初始化项目失败 -> " + err.Error())
+			os.Exit(1)
+		}
+	}
+
+	if !models.NewItemsets().Exist(1) {
+		item := models.NewItemsets()
+		item.ItemName = "默认项目"
+		item.MemberId = 1
+		if err := item.Save(); err != nil {
+			panic("初始化项目集失败 -> " + err.Error())
+			os.Exit(1)
 		}
 	}
 }

+ 33 - 2
controllers/BookController.go

@@ -144,6 +144,7 @@ func (c *BookController) SaveBook() {
 	enableShare := strings.TrimSpace(c.GetString("enable_share")) == "on"
 	isUseFirstDocument := strings.TrimSpace(c.GetString("is_use_first_document")) == "on"
 	autoSave := strings.TrimSpace(c.GetString("auto_save")) == "on"
+	itemId,_ := c.GetInt("itemId")
 
 	if strings.Count(description, "") > 500 {
 		c.JsonResult(6004, "项目描述不能大于500字")
@@ -157,6 +158,9 @@ func (c *BookController) SaveBook() {
 			c.JsonResult(6005, "最多允许添加10个标签")
 		}
 	}
+	if !models.NewItemsets().Exist(itemId) {
+		c.JsonResult(6006,"项目集不存在")
+	}
 	if editor != "markdown" && editor != "html" {
 		editor = "markdown"
 	}
@@ -170,6 +174,7 @@ func (c *BookController) SaveBook() {
 	book.HistoryCount = historyCount
 	book.IsDownload = 0
 	book.BookPassword = c.GetString("bPassword")
+	book.ItemId = itemId
 
 	if autoRelease {
 		book.AutoRelease = 1
@@ -432,6 +437,7 @@ func (c *BookController) Create() {
 		description := strings.TrimSpace(c.GetString("description", ""))
 		privatelyOwned, _ := strconv.Atoi(c.GetString("privately_owned"))
 		commentStatus := c.GetString("comment_status")
+		itemId, _ := c.GetInt("itemId")
 
 		if bookName == "" {
 			c.JsonResult(6001, "项目名称不能为空")
@@ -451,6 +457,9 @@ func (c *BookController) Create() {
 		if privatelyOwned != 0 && privatelyOwned != 1 {
 			privatelyOwned = 1
 		}
+		if !models.NewItemsets().Exist(itemId) {
+			c.JsonResult(6005, "项目集不存在")
+		}
 		if commentStatus != "open" && commentStatus != "closed" && commentStatus != "group_only" && commentStatus != "registered_only" {
 			commentStatus = "closed"
 		}
@@ -503,6 +512,7 @@ func (c *BookController) Create() {
 		book.IsUseFirstDocument = 1
 		book.IsDownload = 1
 		book.AutoRelease = 0
+		book.ItemId = itemId
 
 		book.Editor = "markdown"
 		book.Theme = "default"
@@ -563,6 +573,7 @@ func (c *BookController) Import() {
 	identify := strings.TrimSpace(c.GetString("identify"))
 	description := strings.TrimSpace(c.GetString("description", ""))
 	privatelyOwned, _ := strconv.Atoi(c.GetString("privately_owned"))
+	itemId, _ := c.GetInt("itemId")
 
 	if bookName == "" {
 		c.JsonResult(6001, "项目名称不能为空")
@@ -576,6 +587,9 @@ func (c *BookController) Import() {
 	if ok, err := regexp.MatchString(`^[a-z]+[a-zA-Z0-9_\-]*$`, identify); !ok || err != nil {
 		c.JsonResult(6003, "项目标识只能包含小写字母、数字,以及“-”和“_”符号,并且只能小写字母开头")
 	}
+	if !models.NewItemsets().Exist(itemId) {
+		c.JsonResult(6007, "项目集不存在")
+	}
 	if strings.Count(identify, "") > 50 {
 		c.JsonResult(6004, "文档标识不能超过50字")
 	}
@@ -612,6 +626,7 @@ func (c *BookController) Import() {
 	book.MemberId = c.Member.MemberId
 	book.CommentCount = 0
 	book.Version = time.Now().Unix()
+	book.ItemId = itemId
 
 	book.Editor = "markdown"
 	book.Theme = "default"
@@ -896,7 +911,7 @@ func (c *BookController) TeamDelete() {
 	teamId, _ := c.GetInt("teamId")
 
 	if teamId <= 0 {
-		c.JsonResult(5001,"参数错误")
+		c.JsonResult(5001, "参数错误")
 	}
 	book, err := c.IsPermission()
 
@@ -915,6 +930,7 @@ func (c *BookController) TeamDelete() {
 	c.JsonResult(0, "OK")
 }
 
+//团队搜索.
 func (c *BookController) TeamSearch() {
 	c.Prepare()
 
@@ -928,7 +944,22 @@ func (c *BookController) TeamSearch() {
 	searchResult, err := models.NewTeamRelationship().FindNotJoinBookByBookIdentify(book.BookId, keyword, 10)
 
 	if err != nil {
-		c.JsonResult(500, err.Error())
+		c.JsonResult(500, err.Error(), searchResult)
+	}
+	c.JsonResult(0, "OK", searchResult)
+
+}
+
+//项目集搜索.
+func (c *BookController) ItemsetsSearch() {
+	c.Prepare()
+
+	keyword := strings.TrimSpace(c.GetString("q"))
+
+	searchResult, err := models.NewItemsets().FindItemsetsByName(keyword, 10)
+
+	if err != nil {
+		c.JsonResult(500, err.Error(), searchResult)
 	}
 	c.JsonResult(0, "OK", searchResult)
 

+ 87 - 0
controllers/ItemsetsController.go

@@ -0,0 +1,87 @@
+package controllers
+
+import (
+	"github.com/lifei6671/mindoc/conf"
+	"github.com/lifei6671/mindoc/models"
+	"github.com/astaxie/beego/orm"
+	"github.com/lifei6671/mindoc/utils/pagination"
+	"github.com/astaxie/beego"
+)
+
+type ItemsetsController struct {
+	BaseController
+}
+func (c *ItemsetsController) Prepare() {
+	c.BaseController.Prepare()
+
+	//如果没有开启你们访问则跳转到登录
+	if !c.EnableAnonymous && c.Member == nil {
+		c.Redirect(conf.URLFor("AccountController.Login"), 302)
+		return
+	}
+}
+func (c *ItemsetsController) Index() {
+	c.Prepare()
+	c.TplName = "items/index.tpl"
+	pageIndex, _ := c.GetInt("page", 0)
+
+	items, totalCount, err := models.NewItemsets().FindToPager(pageIndex, conf.PageSize)
+
+	if err != nil && err != orm.ErrNoRows {
+		c.ShowErrorPage(500, err.Error())
+	}
+	c.Data["TotalPages"] = pageIndex
+	if err == orm.ErrNoRows || len(items) <= 0 {
+		c.Data["Lists"] = items
+		c.Data["PageHtml"] = ""
+		return
+	}
+
+	if totalCount > 0 {
+		pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl())
+		c.Data["PageHtml"] = pager.HtmlPages()
+	} else {
+		c.Data["PageHtml"] = ""
+	}
+
+	c.Data["Lists"] = items
+}
+
+func (c *ItemsetsController) List() {
+	c.Prepare()
+	c.TplName = "items/list.tpl"
+	itemKey := c.Ctx.Input.Param(":key")
+	pageIndex, _ := c.GetInt("page", 1)
+
+	if itemKey == "" {
+		c.Abort("404")
+	}
+	item, err := models.NewItemsets().FindFirst(itemKey)
+
+	if err != nil {
+		if err == orm.ErrNoRows {
+			c.Abort("404")
+		} else {
+			beego.Error(err)
+			c.Abort("500")
+		}
+	}
+	memberId := 0
+	if c.Member != nil {
+		memberId = c.Member.MemberId
+	}
+	searchResult, totalCount, err := models.NewItemsets().FindItemsetsByItemKey(itemKey, pageIndex, conf.PageSize, memberId)
+
+	if err != nil && err != orm.ErrNoRows {
+		c.ShowErrorPage(500, "查询文档列表时出错")
+	}
+	if totalCount > 0 {
+		pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl())
+		c.Data["PageHtml"] = pager.HtmlPages()
+	} else {
+		c.Data["PageHtml"] = ""
+	}
+	c.Data["Lists"] = searchResult
+
+	c.Data["Model"] = item
+}

+ 77 - 0
controllers/ManagerController.go

@@ -1073,6 +1073,8 @@ func (c *ManagerController) TeamSearchBook() {
 	c.JsonResult(0, "OK", searchResult)
 
 }
+
+//删除团队项目.
 func (c *ManagerController) TeamBookDelete() {
 	c.Prepare()
 	teamRelationshipId, _ := c.GetInt("teamRelId")
@@ -1088,3 +1090,78 @@ func (c *ManagerController) TeamBookDelete() {
 	}
 	c.JsonResult(0, "OK")
 }
+
+//项目集列表.
+func (c *ManagerController) Itemsets() {
+	c.Prepare()
+	c.TplName = "manager/itemsets.tpl"
+	pageIndex, _ := c.GetInt("page", 0)
+
+	items, totalCount, err := models.NewItemsets().FindToPager(pageIndex, conf.PageSize)
+
+	if err != nil && err != orm.ErrNoRows {
+		c.ShowErrorPage(500, err.Error())
+	}
+	if err == orm.ErrNoRows || len(items) <= 0 {
+		c.Data["Lists"] = items
+		c.Data["PageHtml"] = ""
+		return
+	}
+
+	if totalCount > 0 {
+		pager := pagination.NewPagination(c.Ctx.Request, totalCount, conf.PageSize, c.BaseUrl())
+		c.Data["PageHtml"] = pager.HtmlPages()
+	} else {
+		c.Data["PageHtml"] = ""
+	}
+
+	c.Data["Lists"] = items
+
+
+}
+
+//编辑或添加项目集.
+func (c *ManagerController) ItemsetsEdit() {
+	c.Prepare()
+	itemId, _ := c.GetInt("itemId")
+	itemName := c.GetString("itemName")
+	itemKey := c.GetString("itemKey")
+	if itemName == "" || itemKey == "" {
+		c.JsonResult(5001, "参数错误")
+	}
+	var item *models.Itemsets
+	var err error
+	if itemId > 0 {
+		if item, err = models.NewItemsets().First(itemId); err != nil {
+			if err == orm.ErrNoRows {
+				c.JsonResult(5002, "项目集不存在")
+			} else {
+				c.JsonResult(5003, "查询项目集出错")
+			}
+		}
+	} else {
+		item = models.NewItemsets()
+	}
+
+	item.ItemKey = itemKey
+	item.ItemName = itemName
+	item.MemberId = c.Member.MemberId
+	item.ModifyAt = c.Member.MemberId
+
+	if err := item.Save(); err != nil {
+		c.JsonResult(5004, err.Error())
+	}
+
+	c.JsonResult(0, "OK")
+}
+
+//删除项目集.
+func (c *ManagerController) ItemsetsDelete() {
+	c.Prepare()
+	itemId, _ := c.GetInt("itemId")
+
+	if err := models.NewItemsets().Delete(itemId); err != nil {
+		c.JsonResult(5001, err.Error())
+	}
+	c.JsonResult(0, "OK")
+}

+ 7 - 2
models/BookModel.go

@@ -31,6 +31,8 @@ type Book struct {
 	BookId int `orm:"pk;auto;unique;column(book_id)" json:"book_id"`
 	// BookName 项目名称.
 	BookName string `orm:"column(book_name);size(500)" json:"book_name"`
+	//所属项目集
+	ItemId   int    `orm:"column(item_id);type(int);default(1)" json:"item_id"`
 	// Identify 项目唯一标识.
 	Identify string `orm:"column(identify);size(100);unique" json:"identify"`
 	//是否是自动发布 0 否/1 是
@@ -112,6 +114,9 @@ func (book *Book) Insert() error {
 	o := orm.NewOrm()
 	//	o.Begin()
 	book.BookName = utils.StripTags(book.BookName)
+	if book.ItemId <= 0 {
+		book.ItemId = 1
+	}
 	_, err := o.Insert(book)
 
 	if err == nil {
@@ -125,7 +130,7 @@ func (book *Book) Insert() error {
 		relationship.MemberId = book.MemberId
 		err = relationship.Insert()
 		if err != nil {
-			logs.Error("插入项目与用户关联 => ", err)
+			logs.Error("插入项目与用户关联 -> ", err)
 			//o.Rollback()
 			return err
 		}
@@ -435,7 +440,7 @@ func (book *Book) ThoroughDeleteBook(id int) error {
 		o.Rollback()
 		return err
 	}
-	_,err = o.Raw(fmt.Sprintf("DELETE FROM %s WHERE book_id=?",NewTeamRelationship().TableNameWithPrefix()), book.BookId).Exec()
+	_, err = o.Raw(fmt.Sprintf("DELETE FROM %s WHERE book_id=?", NewTeamRelationship().TableNameWithPrefix()), book.BookId).Exec()
 	if err != nil {
 		o.Rollback()
 		return err

+ 8 - 0
models/BookResult.go

@@ -33,6 +33,8 @@ var (
 type BookResult struct {
 	BookId         int       `json:"book_id"`
 	BookName       string    `json:"book_name"`
+	ItemId         int       `json:"item_id"`
+	ItemName       string    `json:"item_name"`
 	Identify       string    `json:"identify"`
 	OrderIndex     int       `json:"order_index"`
 	Description    string    `json:"description"`
@@ -203,6 +205,7 @@ func (m *BookResult) ToBookResult(book Book) *BookResult {
 	m.HistoryCount = book.HistoryCount
 	m.IsDownload = book.IsDownload == 0
 	m.AutoSave = book.AutoSave == 1
+	m.ItemId = book.ItemId
 
 	if book.Theme == "" {
 		m.Theme = "default"
@@ -224,6 +227,11 @@ func (m *BookResult) ToBookResult(book Book) *BookResult {
 		m.LastModifyText = member2.Account + " 于 " + doc.ModifyTime.Local().Format("2006-01-02 15:04:05")
 	}
 
+	if m.ItemId > 0 {
+		if item,err := NewItemsets().First(m.ItemId); err == nil {
+			m.ItemName = item.ItemName
+		}
+	}
 	return m
 }
 

+ 251 - 0
models/Itemsets.go

@@ -0,0 +1,251 @@
+package models
+
+import (
+	"time"
+	"github.com/lifei6671/mindoc/conf"
+	"github.com/astaxie/beego/orm"
+	"github.com/astaxie/beego"
+	"errors"
+	"github.com/lifei6671/mindoc/utils/cryptil"
+)
+
+//项目集
+type Itemsets struct {
+	ItemId      int       `orm:"column(item_id);pk;auto;unique" json:"item_id"`
+	ItemName    string    `orm:"column(item_name);size(500)" json:"item_name"`
+	ItemKey     string    `orm:"column(item_key);size(200);unique" json:"item_key"`
+	Description string    `orm:"column(description);type(text);null" json:"description"`
+	MemberId    int       `orm:"column(member_id);size(100)" json:"member_id"`
+	CreateTime  time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
+	ModifyTime  time.Time `orm:"column(modify_time);type(datetime);null;auto_now" json:"modify_time"`
+	ModifyAt    int       `orm:"column(modify_at);type(int)" json:"modify_at"`
+
+	BookNumber       int    `orm:"-" json:"book_number"`
+	CreateTimeString string `orm:"-" json:"create_time_string"`
+}
+
+// TableName 获取对应数据库表名.
+func (item *Itemsets) TableName() string {
+	return "itemsets"
+}
+
+// TableEngine 获取数据使用的引擎.
+func (item *Itemsets) TableEngine() string {
+	return "INNODB"
+}
+func (item *Itemsets) TableNameWithPrefix() string {
+	return conf.GetDatabasePrefix() + item.TableName()
+}
+
+func (item *Itemsets) QueryTable() orm.QuerySeter {
+	return orm.NewOrm().QueryTable(item.TableNameWithPrefix())
+}
+
+func NewItemsets() *Itemsets {
+	return &Itemsets{}
+}
+
+func (item *Itemsets) First(itemId int) (*Itemsets, error) {
+	if itemId <= 0 {
+		return nil, ErrInvalidParameter
+	}
+	err := item.QueryTable().Filter("item_id", itemId).One(item)
+	if err != nil {
+		beego.Error("查询项目集失败 -> item_id=", itemId, err)
+	} else {
+		item.Include()
+	}
+	return item, err
+}
+
+func (item *Itemsets) FindFirst(itemKey string) (*Itemsets,error) {
+	err := item.QueryTable().Filter("item_key",itemKey).One(item)
+	if err != nil {
+		beego.Error("查询项目集失败 -> itemKey=", itemKey, err)
+	} else {
+		item.Include()
+	}
+	return item,err
+}
+
+func (item *Itemsets) Exist(itemId int) bool {
+	return item.QueryTable().Filter("item_id", itemId).Exist()
+}
+
+//保存
+func (item *Itemsets) Save() (err error) {
+
+	if item.ItemName == "" {
+		return errors.New("项目集名称不能为空")
+	}
+	if item.ItemKey == "" {
+		item.ItemKey = cryptil.NewRandChars(16)
+	}
+
+	if item.QueryTable().Filter("item_id__ne", item.ItemId).Filter("item_key", item.ItemKey).Exist() {
+		return errors.New("项目集标识已存在")
+	}
+	if item.ItemId > 0 {
+		_, err = orm.NewOrm().Update(item)
+	} else {
+		_, err = orm.NewOrm().Insert(item)
+	}
+	return
+}
+
+//删除.
+func (item *Itemsets) Delete(itemId int) (err error) {
+	if itemId <= 0 {
+		return ErrInvalidParameter
+	}
+	if itemId == 1 {
+		return errors.New("默认项目集不能删除")
+	}
+	if !item.Exist(itemId) {
+		return errors.New("项目集不存在")
+	}
+	o := orm.NewOrm()
+	if err := o.Begin(); err != nil {
+		beego.Error("开启事物失败 ->", err)
+		return err
+	}
+	_, err = o.QueryTable(item.TableNameWithPrefix()).Filter("item_id", itemId).Delete()
+	if err != nil {
+		beego.Error("删除项目集失败 -> item_id=", itemId, err)
+		o.Rollback()
+	}
+	_, err = o.Raw("update md_books set item_id=1 where item_id=?;", itemId).Exec()
+	if err != nil {
+		beego.Error("删除项目集失败 -> item_id=", itemId, err)
+		o.Rollback()
+	}
+
+	return o.Commit()
+}
+
+func (item *Itemsets) Include() (*Itemsets, error) {
+
+	item.CreateTimeString = item.CreateTime.Format("2006-01-02 15:04:05")
+
+	i, err := NewBook().QueryTable().Filter("item_id", item.ItemId).Count()
+	if err != nil && err != orm.ErrNoRows {
+		return item, err
+	}
+	item.BookNumber = int(i)
+
+	return item, nil
+}
+
+//分页查询.
+func (item *Itemsets) FindToPager(pageIndex, pageSize int) (list []*Itemsets, totalCount int, err error) {
+
+	offset := (pageIndex - 1) * pageSize
+
+	_, err = item.QueryTable().OrderBy("-item_id").Offset(offset).Limit(pageSize).All(&list)
+
+	if err != nil {
+		return
+	}
+
+	c, err := item.QueryTable().Count()
+	if err != nil {
+		return
+	}
+	totalCount = int(c)
+
+	for _, item := range list {
+		item.Include()
+	}
+	return
+}
+
+//根据项目集名称查询.
+func (item *Itemsets) FindItemsetsByName(name string, limit int) (*SelectMemberResult, error) {
+	result := SelectMemberResult{}
+
+	var itemsets []*Itemsets
+	var err error
+	if name == "" {
+		_, err = item.QueryTable().Limit(limit).All(&itemsets)
+
+	} else {
+		_, err = item.QueryTable().Filter("item_name__icontains", name).Limit(limit).All(&itemsets)
+	}
+	if err != nil {
+		beego.Error("查询项目集失败 ->", err)
+		return &result, err
+	}
+
+	items := make([]KeyValueItem, 0)
+
+	for _, m := range itemsets {
+		item := KeyValueItem{}
+		item.Id = m.ItemId
+		item.Text = m.ItemName
+		items = append(items, item)
+	}
+	result.Result = items
+
+	return &result, err
+}
+
+//根据项目集标识查询项目集的项目列表.
+func (item *Itemsets) FindItemsetsByItemKey(key string, pageIndex, pageSize, memberId int) (books []*BookResult, totalCount int, err error){
+	o := orm.NewOrm()
+
+	err = item.QueryTable().Filter("item_key",key).One(item)
+
+	if err != nil {
+		return nil,0,err
+	}
+	offset := (pageIndex - 1) * pageSize
+	//如果是登录用户
+	if memberId > 0 {
+		sql1 := `SELECT COUNT(*)
+FROM md_books AS book
+  LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ?
+  left join (select *
+             from (select book_id,team_member_id,role_id
+                   from md_team_relationship as mtr
+                     left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc )as t group by t.book_id) as team on team.book_id = book.book_id
+WHERE book.item_id = ? AND (relationship_id > 0 OR book.privately_owned = 0 or team.team_member_id > 0)`
+
+		err = o.Raw(sql1, memberId, memberId, item.ItemId).QueryRow(&totalCount)
+		if err != nil {
+			return
+		}
+		sql2 := `SELECT book.*,rel1.*,member.account AS create_name FROM md_books AS book
+			LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.member_id = ?
+			left join (select * from (select book_id,team_member_id,role_id
+                   	from md_team_relationship as mtr
+					left join md_team_member as mtm on mtm.team_id=mtr.team_id and mtm.member_id=? order by role_id desc )as t group by t.book_id) as team 
+					on team.book_id = book.book_id
+			LEFT JOIN md_relationship AS rel1 ON rel1.book_id = book.book_id AND rel1.role_id = 0
+			LEFT JOIN md_members AS member ON rel1.member_id = member.member_id
+			WHERE book.item_id = ? AND (rel.relationship_id > 0 OR book.privately_owned = 0 or team.team_member_id > 0) 
+			ORDER BY order_index DESC ,book.book_id DESC LIMIT ?,?`
+
+		_, err = o.Raw(sql2, memberId, memberId, item.ItemId, offset, pageSize).QueryRows(&books)
+
+		return
+
+	} else {
+		count, err1 := o.QueryTable(NewBook().TableNameWithPrefix()).Filter("privately_owned", 0).Filter("item_id", item.ItemId).Count()
+
+		if err1 != nil {
+			err = err1
+			return
+		}
+		totalCount = int(count)
+
+		sql := `SELECT book.*,rel.*,member.account AS create_name FROM md_books AS book
+			LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0
+			LEFT JOIN md_members AS member ON rel.member_id = member.member_id
+			WHERE book.item_id = ? AND book.privately_owned = 0 ORDER BY order_index DESC ,book.book_id DESC LIMIT ?,?`
+
+		_, err = o.Raw(sql, item.ItemId, offset, pageSize).QueryRows(&books)
+
+		return
+
+	}
+}

+ 10 - 0
routers/router.go

@@ -57,6 +57,10 @@ func init() {
 	beego.Router("/manager/team/book/delete", &controllers.ManagerController{}, "POST:TeamBookDelete")
 	beego.Router("/manager/team/book/search", &controllers.ManagerController{}, "*:TeamSearchBook")
 
+	beego.Router("/manager/itemsets", &controllers.ManagerController{},"*:Itemsets")
+	beego.Router("/manager/itemsets/edit", &controllers.ManagerController{},"post:ItemsetsEdit")
+	beego.Router("/manager/itemsets/delete", &controllers.ManagerController{},"post:ItemsetsDelete")
+
 
 	beego.Router("/setting", &controllers.SettingController{}, "*:Index")
 	beego.Router("/setting/password", &controllers.SettingController{}, "*:Password")
@@ -72,6 +76,8 @@ func init() {
 
 
 	beego.Router("/book/create", &controllers.BookController{}, "*:Create")
+	beego.Router("/book/itemsets/search", &controllers.BookController{}, "*:ItemsetsSearch")
+
 	beego.Router("/book/users/create", &controllers.BookMemberController{}, "post:AddMember")
 	beego.Router("/book/users/change", &controllers.BookMemberController{}, "post:ChangeRole")
 	beego.Router("/book/users/delete", &controllers.BookMemberController{}, "post:RemoveMember")
@@ -139,4 +145,8 @@ func init() {
 
 	beego.Router("/tag/:key", &controllers.LabelController{}, "get:Index")
 	beego.Router("/tags", &controllers.LabelController{}, "get:List")
+
+	beego.Router("/items", &controllers.ItemsetsController{},"get:Index")
+	beego.Router("/items/:key", &controllers.ItemsetsController{},"get:List")
+
 }

+ 34 - 2
utils/cryptil/cryptil.go

@@ -13,6 +13,8 @@ import (
 	"crypto/rand"
 )
 
+var stdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
+
 //对称加密与解密之加密【从Beego中提取出来的】
 //@param            value           需要加密的字符串
 //@param            secret          加密密钥
@@ -71,7 +73,6 @@ func Sha1Crypt(str string, salt ...interface{}) (CryptStr string) {
 	return fmt.Sprintf("%x", sha1.Sum([]byte(str)))
 }
 
-
 //生成Guid字串
 func UniqueId() string {
 	b := make([]byte, 48)
@@ -80,4 +81,35 @@ func UniqueId() string {
 		return ""
 	}
 	return Md5Crypt(base64.URLEncoding.EncodeToString(b))
-}
+}
+
+//生成指定长度的字符串.
+func NewRandChars(length int) string {
+	if length == 0 {
+		return ""
+	}
+	clen := len(stdChars)
+	if clen < 2 || clen > 256 {
+		panic("Wrong charset length for NewLenChars()")
+	}
+	maxrb := 255 - (256 % clen)
+	b := make([]byte, length)
+	r := make([]byte, length+(length/4)) // storage for random bytes.
+	i := 0
+	for {
+		if _, err := rand.Read(r); err != nil {
+			panic("Error reading random bytes: " + err.Error())
+		}
+		for _, rb := range r {
+			c := int(rb)
+			if c > maxrb {
+				continue // Skip this number to avoid modulo bias.
+			}
+			b[i] = stdChars[c%clen]
+			i++
+			if i == length {
+				return string(b)
+			}
+		}
+	}
+}

+ 72 - 3
views/book/index.tpl

@@ -12,6 +12,7 @@
     <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet" type="text/css">
     <link href="{{cdncss "/static/bootstrap/plugins/bootstrap-fileinput/4.4.7/css/fileinput.min.css"}}" rel="stylesheet" type="text/css">
     <link href="{{cdncss "/static/bootstrap/plugins/bootstrap-fileinput/4.4.7/themes/explorer-fa/theme.css"}}" rel="stylesheet" type="text/css">
+    <link href="{{cdncss "/static/select2/4.0.5/css/select2.min.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/main.css" "version"}}" 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:// -->
@@ -137,8 +138,19 @@
             <div class="modal-body">
                 <div class="form-group">
                     <div class="pull-left" style="width: 620px">
-                        <div class="form-group">
-                            <input type="text" class="form-control" placeholder="标题(不超过100字)" name="book_name" id="bookName">
+                        <div class="form-group required">
+                            <label class="text-label col-sm-2">项目集</label>
+                            <div class="col-sm-10">
+                                <select class="js-data-example-ajax-add form-control" multiple="multiple" name="itemId" id="itemId"></select>
+                            </div>
+                            <div class="clearfix"></div>
+                        </div>
+                        <div class="form-group required">
+                            <label class="text-label col-sm-2">项目标题</label>
+                            <div class="col-sm-10">
+                                <input type="text" class="form-control" placeholder="标题(不超过100字)" name="book_name" id="bookName">
+                            </div>
+                            <div class="clearfix"></div>
                         </div>
                         <div class="form-group">
                             <div class="pull-left" style="padding: 7px 5px 6px 0">
@@ -194,6 +206,10 @@
                 </div>
                 <div class="modal-body">
                     <div class="form-group">
+                        <div class="form-group required">
+                            <label class="text-label">项目集</label>
+                            <select class="js-data-example-ajax-import form-control" multiple="multiple" name="itemId"></select>
+                        </div>
                         <div class="form-group required">
                             <label class="text-label">项目标题</label>
                             <input type="text" class="form-control" placeholder="项目标题(不超过100字)" name="book_name" maxlength="100" value="">
@@ -272,6 +288,8 @@
 <script src="{{cdnjs "/static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/fileinput.min.js"}}"></script>
 <script src="{{cdnjs "/static/bootstrap/plugins/bootstrap-fileinput/4.4.7/js/locales/zh.js"}}"></script>
 <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript" ></script>
+<script src="{{cdnjs "/static/select2/4.0.5/js/select2.full.min.js"}}"></script>
+<script src="{{cdnjs "/static/select2/4.0.5/js/i18n/zh-CN.js"}}"></script>
 <script src="{{cdnjs "/static/js/main.js"}}" type="text/javascript"></script>
 <script type="text/javascript">
     /**
@@ -400,6 +418,28 @@
         $("#addBookDialogModal").on("show.bs.modal",function () {
             window.bookDialogModal = $(this).find("#addBookDialogForm").html();
             drawBookCover("bookCover","默认封面");
+            $('.js-data-example-ajax-add').select2({
+                language: "zh-CN",
+                minimumInputLength : 1,
+                minimumResultsForSearch: Infinity,
+                maximumSelectionLength:1,
+                width : "100%",
+                ajax: {
+                    url: '{{urlfor "BookController.ItemsetsSearch"}}',
+                    dataType: 'json',
+                    data: function (params) {
+                        return {
+                            q: params.term, // search term
+                            page: params.page
+                        };
+                    },
+                    processResults: function (data, params) {
+                        return {
+                            results : data.data.results
+                        }
+                    }
+                }
+            });
         }).on("hidden.bs.modal",function () {
             $(this).find("#addBookDialogForm").html(window.bookDialogModal);
         });
@@ -429,6 +469,28 @@
                     return book;
                 }
             });
+            $('.js-data-example-ajax-import').select2({
+                language: "zh-CN",
+                minimumInputLength : 1,
+                minimumResultsForSearch: Infinity,
+                maximumSelectionLength:1,
+                width : "100%",
+                ajax: {
+                    url: '{{urlfor "BookController.ItemsetsSearch"}}',
+                    dataType: 'json',
+                    data: function (params) {
+                        return {
+                            q: params.term, // search term
+                            page: params.page
+                        };
+                    },
+                    processResults: function (data, params) {
+                        return {
+                            results : data.data.results
+                        }
+                    }
+                }
+            });
         }).on("hidden.bs.modal",function () {
             $(this).find("#importBookDialogForm").html(window.importBookDialogModal);
         });
@@ -439,7 +501,10 @@
         $("body").on("click","#btnSaveDocument",function () {
             var $this = $(this);
 
-
+            var itemId = $("#itemId").val();
+            if (itemId <= 0) {
+                return showError("请选择项目集")
+            }
             var bookName = $.trim($("#bookName").val());
             if (bookName === "") {
                 return showError("项目标题不能为空")
@@ -504,6 +569,10 @@
             console.log("aa");
             var $then = $(this).parents("#importBookDialogForm");
 
+            var itemId = $then.find("input[name='itemId']").val();
+            if (itemId <= 0) {
+                return showError("请选择项目集")
+            }
 
             var bookName = $.trim($then.find("input[name='book_name']").val());
 

+ 31 - 1
views/book/setting.tpl

@@ -14,6 +14,7 @@
     <link href="{{cdncss "/static/cropper/2.3.4/cropper.min.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/bootstrap/plugins/tagsinput/bootstrap-tagsinput.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/bootstrap/plugins/bootstrap-switch/css/bootstrap3//bootstrap-switch.min.css"}}" rel="stylesheet">
+    <link href="{{cdncss "/static/select2/4.0.5/css/select2.min.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
 
 </head>
@@ -60,6 +61,12 @@
                                 <input type="text" class="form-control" value="{{urlfor "DocumentController.Index" ":key" .Model.Identify}}" placeholder="项目唯一标识" disabled>
                                 <p class="text">项目标识用来标记项目的唯一性,不可修改。</p>
                             </div>
+                            <div class="form-group">
+                                <label>项目集</label>
+                                <select class="js-data-example-ajax form-control" multiple="multiple" name="itemId">
+                                    <option value="{{.Model.ItemId}}" selected="selected">{{.Model.ItemName}}</option>
+                                </select>
+                            </div>
                             <div class="form-group">
                                 <label>历史记录数量</label>
                                 <input type="text" class="form-control" name="history_count" value="{{.Model.HistoryCount}}" placeholder="历史记录数量">
@@ -312,6 +319,8 @@
 <script src="{{cdnjs "/static/js/jquery.form.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/bootstrap/plugins/tagsinput/bootstrap-tagsinput.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/bootstrap/plugins/bootstrap-switch/js/bootstrap-switch.min.js"}}" type="text/javascript"></script>
+<script src="{{cdnjs "/static/select2/4.0.5/js/select2.full.min.js"}}"></script>
+<script src="{{cdnjs "/static/select2/4.0.5/js/i18n/zh-CN.js"}}"></script>
 <script src="{{cdnjs "/static/js/main.js"}}" type="text/javascript"></script>
 <script type="text/javascript">
     $(function () {
@@ -431,7 +440,28 @@
                 $("#btnTransferBook").button("reset");
             }
         });
-
+        $('.js-data-example-ajax').select2({
+            language: "zh-CN",
+            minimumInputLength : 1,
+            minimumResultsForSearch: Infinity,
+            maximumSelectionLength:1,
+            width : "100%",
+            ajax: {
+                url: '{{urlfor "BookController.ItemsetsSearch"}}',
+                dataType: 'json',
+                data: function (params) {
+                    return {
+                        q: params.term, // search term
+                        page: params.page
+                    };
+                },
+                processResults: function (data, params) {
+                    return {
+                        results : data.data.results
+                    }
+                }
+            }
+        });
         try {
             var uploader = WebUploader.create({
                 auto: false,

+ 74 - 0
views/items/index.tpl

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <title>项目集列表 - Powered by MinDoc</title>
+    <meta name="keywords" content="MinDoc,文档在线管理系统,WIKI,wiki,wiki在线,文档在线管理,接口文档在线管理,接口文档管理">
+    <meta name="description" content="MinDoc文档在线管理系统 {{.site_description}}">
+    <meta name="author" content="Minho" />
+    <meta name="site" content="https://www.iminho.me" />
+    <!-- Bootstrap -->
+    <link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
+    <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
+
+    <link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
+</head>
+<body>
+<div class="manual-reader manual-container manual-search-reader">
+{{template "widgets/header.tpl" .}}
+    <div class="container manual-body">
+        <div class="search-head">
+            <strong class="search-title">项目集列表</strong>
+        </div>
+        <div class="row">
+            <div class="hide tag-container-outer" style="border: 0;margin-top: 0;padding: 5px 15px;min-height: 200px;">
+                <div class="attach-list" id="ItemsetsList">
+                    <table class="table">
+                        <thead>
+                        <tr>
+                            <th width="10%">#</th>
+                            <th width="30%">项目集名称</th>
+                            <th width="20%">项目集标识</th>
+                            <th width="10%">项目数量</th>
+                            <th width="15%">创建时间</th>
+                            <th>操作</th>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        {{range $index,$item := .Lists}}
+                        <tr>
+                            <td>{{$item.ItemId}}</td>
+                            <td>{{$item.ItemName}}</td>
+                            <td>{{$item.ItemKey}}</td>
+                            <td>{{$item.BookNumber}}</td>
+                            <td>{{$item.CreateTimeString}}</td>
+                            <td>
+                                <a href="{{urlfor "ItemsetsController.List" ":key" $item.ItemKey}}" class="btn btn-success btn-sm" target="_blank">详情</a>
+                            </td>
+                        </tr>
+                        {{else}}
+                        <tr><td class="text-center" colspan="6">暂无数据</td></tr>
+                        {{end}}
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+
+            <nav class="pagination-container">
+            {{if gt .TotalPages 1}}
+                {{.PageHtml}}
+                {{end}}
+                <div class="clearfix"></div>
+            </nav>
+        </div>
+    </div>
+{{template "widgets/footer.tpl" .}}
+</div>
+<script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}" type="text/javascript"></script>
+<script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
+{{.Scripts}}
+</body>
+</html>

+ 66 - 0
views/items/list.tpl

@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <title>项目集{{.Model.ItemName}}的项目列表 - Powered by MinDoc</title>
+    <meta name="keywords" content="MinDoc,文档在线管理系统,WIKI,wiki,wiki在线,文档在线管理,接口文档在线管理,接口文档管理,{{.Model.ItemName}}">
+    <meta name="description" content="MinDoc文档在线管理系统 {{.site_description}}">
+    <!-- Bootstrap -->
+    <link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
+    <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
+
+    <link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
+</head>
+<body>
+<div class="manual-reader manual-container manual-search-reader">
+{{template "widgets/header.tpl" .}}
+    <div class="container manual-body">
+        <div class="search-head">
+            <strong class="search-title">显示项目集为"{{.Model.ItemName}}"的项目</strong>
+        </div>
+        <div class="row">
+            <div class="manual-list">
+            {{range $index,$item := .Lists}}
+                <div class="list-item">
+                    <dl class="manual-item-standard">
+                        <dt>
+                            <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" title="{{$item.BookName}}-{{$item.CreateName}}" target="_blank">
+                                <img src="{{cdnimg $item.Cover}}" class="cover" alt="{{$item.BookName}}-{{$item.CreateName}}">
+                            </a>
+                        </dt>
+                        <dd>
+                            <a href="{{urlfor "DocumentController.Index" ":key" $item.Identify}}" class="name" title="{{$item.BookName}}-{{$item.CreateName}}" target="_blank">{{$item.BookName}}</a>
+                        </dd>
+                        <dd>
+                            <span class="author">
+                                <b class="text">作者</b>
+                                <b class="text">-</b>
+                                <b class="text">{{if eq $item.RealName "" }}{{$item.CreateName}}{{else}}{{$item.RealName}}{{end}}</b>
+                            </span>
+                        </dd>
+                    </dl>
+                </div>
+            {{else}}
+                <div class="search-empty">
+                    <img src="{{cdnimg "/static/images/search_empty.png"}}" class="empty-image">
+                    <span class="empty-text">暂无项目</span>
+                </div>
+            {{end}}
+
+                <div class="clearfix"></div>
+            </div>
+            <nav class="pagination-container">
+            {{.PageHtml}}
+            </nav>
+        </div>
+    </div>
+{{template "widgets/footer.tpl" .}}
+</div>
+<script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
+<script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
+{{.Scripts}}
+</body>
+</html>

+ 257 - 0
views/manager/itemsets.tpl

@@ -0,0 +1,257 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <title>项目集管理 - Powered by MinDoc</title>
+
+    <!-- Bootstrap -->
+    <link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet" type="text/css">
+    <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet" type="text/css">
+
+    <link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
+</head>
+<body>
+<div class="manual-reader">
+{{template "widgets/header.tpl" .}}
+    <div class="container manual-body">
+        <div class="row">
+        {{template "manager/widgets.tpl" "itemsets"}}
+            <div class="page-right">
+                <div class="m-box">
+                    <div class="box-head">
+                        <strong class="box-title">项目集管理</strong>
+                    {{if eq .Member.Role 0}}
+                        <button type="button" class="btn btn-success btn-sm pull-right" data-toggle="modal" data-target="#addItemsetsDialogModal"><i class="fa fa-plus" aria-hidden="true"></i> 创建项目集</button>
+                    {{end}}
+                    </div>
+                </div>
+                <div class="box-body">
+                    <div class="attach-list" id="ItemsetsList">
+                        <table class="table">
+                            <thead>
+                            <tr>
+                                <th width="10%">#</th>
+                                <th width="30%">项目集名称</th>
+                                <th width="20%">项目集标识</th>
+                                <th width="20%">项目数量</th>
+                                <th>操作</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            {{range $index,$item := .Lists}}
+                            <tr>
+                                <td>{{$item.ItemId}}</td>
+                                <td>{{$item.ItemName}}</td>
+                                <td>{{$item.ItemKey}}</td>
+                                <td>{{$item.BookNumber}}</td>
+                                <td>
+                                    <button type="button" class="btn btn-sm btn-default" data-id="{{$item.ItemId}}" data-method="edit" data-name="{{$item.ItemName}}" data-key="{{$item.ItemKey}}">编辑</button>
+                                    <button type="button" data-method="delete" class="btn btn-danger btn-sm" data-id="{{$item.ItemId}}" data-loading-text="删除中...">删除</button>
+                                    <a href="{{urlfor "ItemsetsController.Index" ":key" $item.ItemKey}}" class="btn btn-success btn-sm" target="_blank">详情</a>
+                                </td>
+                            </tr>
+                            {{else}}
+                            <tr><td class="text-center" colspan="6">暂无数据</td></tr>
+                            {{end}}
+                            </tbody>
+                        </table>
+                        <nav class="pagination-container">
+                        {{.PageHtml}}
+                        </nav>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+{{template "widgets/footer.tpl" .}}
+</div>
+<!-- Modal -->
+<div class="modal fade" id="addItemsetsDialogModal" tabindex="-1" role="dialog" aria-labelledby="addItemsetsDialogModalLabel">
+    <div class="modal-dialog">
+        <form method="post" autocomplete="off" class="form-horizontal" action="{{urlfor "ManagerController.ItemsetsEdit"}}" id="addItemsetsDialogForm">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                    <h4 class="modal-title" id="myModalLabel">创建项目集</h4>
+                </div>
+                <div class="modal-body">
+                    <div class="form-group">
+                        <label class="col-sm-3 control-label" for="account">项目集名称<span class="error-message">*</span></label>
+                        <div class="col-sm-9">
+                            <input type="text" name="itemName" class="form-control" placeholder="项目集名称" id="itemName" maxlength="50">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="col-sm-3 control-label" for="itemKey">项目集标识<span class="error-message">*</span></label>
+                        <div class="col-sm-9">
+                            <input type="text" name="itemKey" id="itemKey" class="form-control" placeholder="项目集标识" maxlength="50">
+                            <p class="text">项目集标识只能由字母和数字组成且在2-100字符之间</p>
+                        </div>
+                    </div>
+                    <div class="clearfix"></div>
+                </div>
+                <div class="modal-footer">
+                    <span id="create-form-error-message"></span>
+                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
+                    <button type="submit" class="btn btn-success" data-loading-text="保存中..." id="btnAddItemsets">保存
+                    </button>
+                </div>
+            </div>
+        </form>
+    </div>
+</div><!--END Modal-->
+<div class="modal fade" id="editItemsetsDialogModal" tabindex="-1" role="dialog" aria-labelledby="editItemsetsDialogModalLabel">
+    <div class="modal-dialog" role="document">
+        <form method="post" autocomplete="off" class="form-horizontal" action="{{urlfor "ManagerController.ItemsetsEdit"}}" id="editItemsetsDialogForm">
+            <input type="hidden" name="itemId" value="">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                    <h4 class="modal-title" id="myModalLabel">编辑项目集</h4>
+                </div>
+                <div class="modal-body">
+                    <div class="form-group">
+                        <label class="col-sm-2 control-label" for="itemName">项目集名称<span class="error-message">*</span></label>
+                        <div class="col-sm-10">
+                            <input type="text" name="itemName" id="itemName" class="form-control" placeholder="项目集名称" maxlength="50">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label class="col-sm-2 control-label" for="itemKey">项目集标识<span class="error-message">*</span></label>
+                        <div class="col-sm-10">
+                            <input type="text" name="itemKey" id="itemKey" class="form-control" placeholder="项目集标识" maxlength="50">
+                        </div>
+                    </div>
+                    <div class="clearfix"></div>
+                </div>
+                <div class="modal-footer">
+                    <span id="edit-form-error-message"></span>
+                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
+                    <button type="submit" class="btn btn-success" data-loading-text="保存中..." id="btnEditItemsets">保存
+                    </button>
+                </div>
+            </div>
+        </form>
+    </div>
+</div><!--END Modal-->
+<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="{{cdnjs "/static/layer/layer.js" }}" type="text/javascript"></script>
+<script src="{{cdnjs "/static/js/main.js"}}" type="text/javascript"></script>
+
+<script type="text/javascript">
+    $(function () {
+        var editItemsetsDialogModal = $("#editItemsetsDialogModal");
+        var addItemsetsDialogForm = $("#addItemsetsDialogForm");
+        var editItemsetsDialogForm = $("#editItemsetsDialogForm");
+
+        editItemsetsDialogModal.on("shown.bs.modal",function () {
+            editItemsetsDialogModal.find("input[name='itemName']").focus();
+        });
+        $("#addItemsetsDialogModal").on("show.bs.modal", function () {
+            window.addItemsetsDialogModalHtml = $(this).find("form").html();
+        }).on("hidden.bs.modal", function () {
+            $(this).find("form").html(window.addItemsetsDialogModalHtml);
+        });
+
+        addItemsetsDialogForm.ajaxForm({
+            beforeSubmit: function () {
+                var $itemName = addItemsetsDialogForm.find("input[name='itemName']").val();
+                var $itemKey =  addItemsetsDialogForm.find("input[name='itemKey']").val();
+
+                if ($itemName == "") {
+                    showError("项目集名称不能为空","#create-form-error-message");
+                }
+                if ($itemKey == "") {
+                    showError("项目集标识不能为空","#create-form-error-message");
+                }
+                $("#btnAddItemsets").button("loading");
+                showError("","#create-form-error-message");
+                return true;
+            },
+            success: function ($res) {
+                if ($res.errcode === 0) {
+                    window.location = window.document.location;
+                } else {
+                    showError($res.message,"#create-form-error-message");
+                }
+            },
+            error: function () {
+                showError("服务器异常","#create-form-error-message");
+            },
+            complete: function () {
+                $("#btnAddItemsets").button("reset");
+            }
+        });
+
+        editItemsetsDialogForm.ajaxForm({
+           beforeSubmit: function () {
+               var $itemName = editItemsetsDialogForm.find("input[name='itemName']").val();
+               var $itemKey =  editItemsetsDialogForm.find("input[name='itemKey']").val();
+
+               if ($itemName == "") {
+                   showError("项目集名称不能为空","#edit-form-error-message");
+               }
+               if ($itemKey == "") {
+                   showError("项目集标识不能为空","#edit-form-error-message");
+               }
+               $("#btnEditItemsets").button("loading");
+               showError("","#edit-form-error-message");
+               return true;
+           } ,
+            success : function ($res) {
+                if ($res.errcode === 0) {
+                    window.location = window.document.location;
+                } else {
+                    showError($res.message,"#edit-form-error-message");
+                }
+            },
+            error: function () {
+                showError("服务器异常","#edit-form-error-message");
+            },
+            complete: function () {
+                $("#btnEditItemsets").button("reset");
+            }
+        });
+
+        $("#ItemsetsList").on("click","button[data-method='delete']",function () {
+            var id = $(this).attr("data-id");
+            var $this = $(this);
+            $(this).button("loading");
+            $.ajax({
+                url : "{{urlfor "ManagerController.ItemsetsDelete" ":id" ""}}" + id,
+                type : "post",
+                dataType : "json",
+                success : function (res) {
+                    if(res.errcode === 0){
+                        $this.closest("tr").remove().empty();
+                    }else {
+                        layer.msg(res.message);
+                    }
+                },
+                error : function () {
+                    layer.msg("服务器异常");
+                },
+                complete : function () {
+                    $this.button("reset");
+                }
+            });
+        }).on("click","button[data-method='edit']",function () {
+            var $itemId = $(this).attr("data-id");
+            var $itemName = $(this).attr("data-name");
+            var $itemKey = $(this).attr("data-key");
+
+            editItemsetsDialogModal.find("input[name='itemId']").val($itemId);
+            editItemsetsDialogModal.find("input[name='itemName']").val($itemName);
+            editItemsetsDialogModal.find("input[name='itemKey']").val($itemKey);
+
+            editItemsetsDialogModal.modal("show");
+        });
+    });
+</script>
+</body>
+</html>

+ 2 - 0
views/manager/widgets.tpl

@@ -4,6 +4,8 @@
         <li{{if eq "users" .}} class="active"{{end}}><a href="{{urlfor "ManagerController.Users" }}" class="item"><i class="fa fa-user" aria-hidden="true"></i> 用户管理</a> </li>
         <li{{if eq "team" .}} class="active"{{end}}><a href="{{urlfor "ManagerController.Team" }}" class="item"><i class="fa fa-group" aria-hidden="true"></i> 团队管理</a> </li>
         <li{{if eq "books" .}} class="active"{{end}}><a href="{{urlfor "ManagerController.Books" }}" class="item"><i class="fa fa-book" aria-hidden="true"></i> 项目管理</a> </li>
+        <li{{if eq "itemsets" .}} class="active"{{end}}><a href="{{urlfor "ManagerController.Itemsets" }}" class="item"><i class="fa fa-archive" aria-hidden="true"></i> 项目集管理</a> </li>
+
     {{/*<li><a href="{{urlfor "ManagerController.Comments" }}" class="item"><i class="fa fa-comments-o" aria-hidden="true"></i> 评论管理</a> </li>*/}}
         <li{{if eq "setting" .}} class="active"{{end}}><a href="{{urlfor "ManagerController.Setting" }}" class="item"><i class="fa fa-cogs" aria-hidden="true"></i> 配置管理</a> </li>
         {{/*<li{{if eq "config" .}} class="active"{{end}}><a href="{{urlfor "ManagerController.Config" }}" class="item"><i class="fa fa-file" aria-hidden="true"></i> 配置文件</a> </li>*/}}

+ 2 - 2
views/widgets/header.tpl

@@ -16,8 +16,8 @@
                     <li {{if eq .ControllerName "BlogController"}}{{if eq  .ActionName "List" "Index"}}class="active"{{end}}{{end}}>
                         <a href="{{urlfor "BlogController.List" }}" title="文章">文章</a>
                     </li>
-                    <li {{if eq .ControllerName "LabelController"}}class="active"{{end}}>
-                        <a href="{{urlfor "LabelController.List" }}" title="标签">标签</a>
+                    <li {{if eq .ControllerName "ItemsetsController"}}class="active"{{end}}>
+                        <a href="{{urlfor "ItemsetsController.Index" }}" title="项目集">项目集</a>
                     </li>
                 </ul>
                 <div class="searchbar pull-left visible-lg-inline-block visible-md-inline-block">