From 90d1f4bb2e780a712ac0a8478be1ed97aec38fd1 Mon Sep 17 00:00:00 2001 From: sunface Date: Wed, 11 Sep 2019 11:15:03 +0800 Subject: [PATCH] fix #27, add discuss component --- internal/api_handler.go | 10 +- internal/ecode/ecode.go | 6 + internal/post/comment.go | 550 +++++++++++++++++++++++++++- internal/post/const.go | 21 ++ internal/post/modify.go | 75 ++++ internal/post/post.go | 66 ---- internal/utils/time.go | 6 +- quick-start/cql/start.cql | 27 +- ui/src/assets/icon/iconfont.css | 37 ++ ui/src/assets/icon/iconfont.eot | Bin 0 -> 2288 bytes ui/src/assets/icon/iconfont.svg | 41 +++ ui/src/assets/icon/iconfont.ttf | Bin 0 -> 2120 bytes ui/src/assets/icon/iconfont.woff | Bin 0 -> 1424 bytes ui/src/assets/icons/downvote.svg | 1 + ui/src/assets/icons/upvote.svg | 1 + ui/src/main.js | 2 + ui/src/router/index.js | 2 +- ui/src/theme/light/style.less | 14 +- ui/src/theme/md_render.css | 47 +-- ui/src/theme/style.less | 4 + ui/src/utils/request.js | 13 +- ui/src/views/article/detail.vue | 12 +- ui/src/views/article/edit.vue | 2 +- ui/src/views/components/discuss.vue | 390 ++++++++++++++++++-- ui/src/views/components/editor.vue | 14 +- ui/src/views/components/render.vue | 4 +- ui/src/views/nav.vue | 2 +- 27 files changed, 1165 insertions(+), 182 deletions(-) create mode 100644 internal/post/modify.go create mode 100644 ui/src/assets/icon/iconfont.css create mode 100644 ui/src/assets/icon/iconfont.eot create mode 100644 ui/src/assets/icon/iconfont.svg create mode 100644 ui/src/assets/icon/iconfont.ttf create mode 100644 ui/src/assets/icon/iconfont.woff create mode 100644 ui/src/assets/icons/downvote.svg create mode 100644 ui/src/assets/icons/upvote.svg diff --git a/internal/api_handler.go b/internal/api_handler.go index 3830ecdb..52648bf7 100644 --- a/internal/api_handler.go +++ b/internal/api_handler.go @@ -20,6 +20,12 @@ func apiHandler(e *echo.Echo) { e.POST("/web/article/saveChanges", post.SaveArticleChanges, session.CheckSignIn) // comment apis - e.POST("/web/post/comment", post.Comment, session.CheckSignIn) - e.GET("/web/post/queryComments", post.QueryComments) + e.POST("/web/comment/create", post.Comment, session.CheckSignIn) + e.POST("/web/comment/reply", post.CommentReply, session.CheckSignIn) + e.POST("/web/comment/edit", post.EditComment, session.CheckSignIn) + e.POST("/web/comment/delete", post.DeleteComment, session.CheckSignIn) + e.POST("/web/comment/revert", post.RevertComment, session.CheckSignIn) + e.GET("/web/comment/query", post.QueryComments) + e.POST("/web/comment/like", post.CommentLike, session.CheckSignIn) + e.POST("/web/comment/dislike", post.CommentDislike, session.CheckSignIn) } diff --git a/internal/ecode/ecode.go b/internal/ecode/ecode.go index 2243638b..43cd40e2 100644 --- a/internal/ecode/ecode.go +++ b/internal/ecode/ecode.go @@ -23,3 +23,9 @@ const ( PostNotFound = 1101 PostNotFoundMsg = "Target post not found" ) + +// comment +const ( + CommentLiked = 1200 + CommentLikedMsg = "You have agreed this comment before" +) diff --git a/internal/post/comment.go b/internal/post/comment.go index 02fac510..313bfbd8 100644 --- a/internal/post/comment.go +++ b/internal/post/comment.go @@ -2,11 +2,14 @@ package post import ( "encoding/json" + "fmt" "net/http" "sort" "strconv" + "strings" "time" + "github.com/gocql/gocql" "github.com/labstack/echo" "github.com/thinkindev/im.dev/internal/ecode" "github.com/thinkindev/im.dev/internal/misc" @@ -18,14 +21,23 @@ import ( // CommentContent holds the comment content type CommentContent struct { ID string `json:"id"` + PID string `json:"pid"` + Depth int `json:"depth"` MD string `json:"md"` Render string `json:"render"` UID string `json:"uid"` UName string `json:"uname"` UNickname string `json:"unickname"` - UAvatar string `json:"uavatar"` PublishDate string `json:"date"` publishDate int64 + + EditDate string `json:"edit_date,omitempty"` + editDate int64 + + Likes int `json:"likes"` + Liked int `json:"liked"` // current login user's liked to the comment, 0 normal, 1 liked, 2 disliked + + Status int `json:"status"` } // Comments is the list of comment @@ -38,7 +50,7 @@ func (a Comments) Swap(i, j int) { // 重写 Swap() 方法 a[i], a[j] = a[j], a[i] } func (a Comments) Less(i, j int) bool { // 重写 Less() 方法, 从小到大排序 - return a[i].publishDate < a[j].publishDate + return a[i].publishDate >= a[j].publishDate } // Comment is the action that user make commention to one post @@ -89,11 +101,15 @@ func Comment(c echo.Context) error { cc.UID = sess.ID // generate id for article cc.ID = misc.GenID() + cc.UName = sess.Name + cc.UNickname = sess.NickName // modify render cc.Render = modify(cc.Render) + cc.publishDate = time.Now().Unix() + cc.PublishDate = utils.Time2ReadableString(time.Unix(cc.publishDate, 0)) err = misc.CQL.Query("INSERT INTO comment (id,uid,post_id,post_type,md,render,publish_date) VALUES (?,?,?,?,?,?,?)", - cc.ID, sess.ID, postID, postType, cc.MD, cc.Render, time.Now().Unix()).Exec() + cc.ID, sess.ID, postID, postType, cc.MD, cc.Render, cc.publishDate).Exec() if err != nil { misc.Log.Warn("access database error", zap.Error(err)) return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ @@ -116,6 +132,249 @@ func Comment(c echo.Context) error { }) } +// CommentReply is the action that user make commention to one post +func CommentReply(c echo.Context) error { + pid := c.FormValue("pid") + postID := c.FormValue("post_id") + postType, _ := strconv.Atoi(c.FormValue("post_type")) + content := c.FormValue("content") + + if pid == "" || postID == "" || postType != ArticleType || content == "" { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + // check whether target post exist + var title string + var err error + switch postType { + case ArticleType: + err = misc.CQL.Query("SELECT title FROM article WHERE id=?", postID).Scan(&title) + } + if err != nil { + if err.Error() == misc.CQLNotFound { + return c.JSON(http.StatusNotFound, misc.HTTPResp{ + ErrCode: ecode.PostNotFound, + Message: ecode.PostNotFoundMsg, + }) + } + misc.Log.Warn("access database error", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + cc := &CommentContent{} + err = json.Unmarshal([]byte(content), &cc) + if err != nil { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := session.Get(c) + + cc.UID = sess.ID + // generate id for article + cc.ID = misc.GenID() + cc.UName = sess.Name + cc.UNickname = sess.NickName + // modify render + cc.Render = modify(cc.Render) + cc.publishDate = time.Now().Unix() + cc.PublishDate = utils.Time2ReadableString(time.Unix(cc.publishDate, 0)) + cc.PID = pid + err = misc.CQL.Query("INSERT INTO comment (id,pid,uid,post_id,post_type,md,render,publish_date) VALUES (?,?,?,?,?,?,?,?)", + cc.ID, pid, sess.ID, postID, postType, cc.MD, cc.Render, cc.publishDate).Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + // update post comment count + switch postType { + case ArticleType: + err = misc.CQL.Query("UPDATE post_counter SET comments=comments + 1 WHERE id=?", postID).Exec() + } + if err != nil { + misc.Log.Warn("access database error", zap.Error(err)) + } + + return c.JSON(http.StatusOK, misc.HTTPResp{ + Data: cc, + }) +} + +// EditComment edit the comment +func EditComment(c echo.Context) error { + id := c.FormValue("id") + content := c.FormValue("content") + + if id == "" || content == "" { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := session.Get(c) + + // check permission + q := misc.CQL.Query(`SELECT uid FROM comment WHERE id=?`, id) + var uid string + err := q.Scan(&uid) + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + if uid != sess.ID { + return c.JSON(http.StatusUnauthorized, misc.HTTPResp{ + ErrCode: ecode.NoPermission, + Message: ecode.NoPermissionMsg, + }) + } + + cc := &CommentContent{} + err = json.Unmarshal([]byte(content), &cc) + if err != nil { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + cc.Render = modify(cc.Render) + + q = misc.CQL.Query(`UPDATE comment SET md=?,render=?,edit_date=? WHERE id=?`, cc.MD, cc.Render, time.Now().Unix(), id) + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + return c.JSON(http.StatusOK, misc.HTTPResp{ + Data: cc.Render, + }) +} + +// DeleteComment delete the comment +// Only comment author can do this +func DeleteComment(c echo.Context) error { + id := c.FormValue("id") + if id == "" { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := session.Get(c) + // check comment exists and this user has permission + var uid string + q := misc.CQL.Query(`SELECT uid FROM comment WHERE id=?`, id) + err := q.Scan(&uid) + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + if sess.ID != uid { + return c.JSON(http.StatusUnauthorized, misc.HTTPResp{ + ErrCode: ecode.NoPermission, + Message: ecode.NoPermissionMsg, + }) + } + + // set comment to delete status + q = misc.CQL.Query(`UPDATE comment SET status=? WHERE id=?`, StatusDeleted, id) + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + return c.JSON(http.StatusOK, misc.HTTPResp{}) +} + +// RevertComment revert the comment from delete status +// Only comment owner and post author can do this +func RevertComment(c echo.Context) error { + id := c.FormValue("id") + if id == "" { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := session.Get(c) + // check comment exists and this user has permission + var uid, md, render string + + q := misc.CQL.Query(`SELECT uid,md,render FROM comment WHERE id=?`, id) + err := q.Scan(&uid, &md, &render) + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + if sess.ID != uid { + return c.JSON(http.StatusUnauthorized, misc.HTTPResp{ + ErrCode: ecode.NoPermission, + Message: ecode.NoPermissionMsg, + }) + } + + // set comment to delete status + q = misc.CQL.Query(`UPDATE comment SET status=? WHERE id=?`, StatusNormal, id) + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + comment := &CommentContent{} + comment.MD = md + comment.Render = render + u := session.GetUserByID(uid) + if u == nil { + comment.UName = "[404]" + comment.UNickname = "[404]" + } else { + comment.UName = u.Name + comment.UNickname = u.NickName + } + + return c.JSON(http.StatusOK, misc.HTTPResp{ + Data: comment, + }) +} + // QueryComments return the comments by post id func QueryComments(c echo.Context) error { postID := c.FormValue("post_id") @@ -126,34 +385,61 @@ func QueryComments(c echo.Context) error { }) } - q := misc.CQL.Query(`SELECT id,uid,md,render,publish_date FROM comment WHERE post_id=?`, postID) + q := misc.CQL.Query(`SELECT id,pid,uid,md,render,publish_date,edit_date,status FROM comment WHERE post_id=?`, postID) iter := q.Iter() - comments := make(Comments, 0) - var cid, uid, md, render string - var pdate int64 - for iter.Scan(&cid, &uid, &md, &render, &pdate) { + // comment map + cMap := make(map[string]*CommentContent) + // parent -> child map + pMap := make(map[string][]string) + // no parent list + nopList := make([]string, 0) + + var cid, uid, md, render, pid string + var pdate, edate int64 + var status int + for iter.Scan(&cid, &pid, &uid, &md, &render, &pdate, &edate, &status) { comment := &CommentContent{ ID: cid, + PID: pid, UID: uid, MD: md, Render: render, publishDate: pdate, + editDate: edate, + Status: status, } u := session.GetUserByID(comment.UID) if u == nil { continue } - comment.UName = u.Name - comment.UNickname = u.NickName - comment.UAvatar = u.Avatar - comment.PublishDate = utils.Time2ReadableString(time.Unix(comment.publishDate, 0)) + if status == StatusDeleted { + comment.MD = "[deleted]" + comment.Render = "[deleted]" + comment.UName = "[deleted]" + comment.UNickname = "[deleted]" + } else { + comment.UName = u.Name + comment.UNickname = u.NickName + } - comments = append(comments, comment) + comment.PublishDate = utils.Time2ReadableString(time.Unix(comment.publishDate, 0)) + if comment.editDate != 0 { + comment.EditDate = utils.Time2ReadableString(time.Unix(comment.editDate, 0)) + } + cMap[comment.ID] = comment + if comment.PID == "" { + nopList = append(nopList, comment.ID) + } else { + childs, ok := pMap[comment.PID] + if !ok { + pMap[comment.PID] = []string{comment.ID} + } else { + pMap[comment.PID] = append(childs, comment.ID) + } + } } - sort.Sort(comments) - if err := iter.Close(); err != nil { misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ @@ -162,7 +448,241 @@ func QueryComments(c echo.Context) error { }) } + // defualt sort based on time ascending + sort.Strings(nopList) + for _, childs := range pMap { + sort.Strings(childs) + } + + comments := make([]*CommentContent, 0) + + for _, nop := range nopList { + // add first level comment to final list + comment := cMap[nop] + comment.Depth = 0 + comments = append(comments, comment) + + // recursively find his childs + findCommentChilds(&comments, nop, pMap, cMap, 1) + } + + var b strings.Builder + b.WriteString(`SELECT id,likes FROM comment_counter WHERE id in (`) + + var b1 strings.Builder + sess := session.Get(c) + if sess != nil { + b1.WriteString(`SELECT comment_id,type FROM comment_like WHERE uid=? and comment_id in (`) + } + for i, c := range comments { + if i == len(comments)-1 { + b.WriteString(fmt.Sprintf("'%s')", c.ID)) + if sess != nil { + b1.WriteString(fmt.Sprintf("'%s')", c.ID)) + } + } else { + b.WriteString(fmt.Sprintf("'%s',", c.ID)) + if sess != nil { + b1.WriteString(fmt.Sprintf("'%s',", c.ID)) + } + } + } + + if len(comments) != 0 { + q = misc.CQL.Query(b.String()) + iter = q.Iter() + + var id string + var likes int + likesMap := make(map[string]int) + for iter.Scan(&id, &likes) { + likesMap[id] = likes + } + + if err := iter.Close(); err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + likedMap := make(map[string]int) + if sess != nil { + q := misc.CQL.Query(b1.String(), sess.ID) + iter := q.Iter() + var liked int + var postID string + for iter.Scan(&postID, &liked) { + likedMap[postID] = liked + } + + if err := iter.Close(); err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + } + for _, c := range comments { + c.Likes = likesMap[c.ID] + if sess != nil { + c.Liked = likedMap[c.ID] + } + } + } + return c.JSON(http.StatusOK, misc.HTTPResp{ Data: comments, }) } + +func findCommentChilds(comments *([]*CommentContent), pid string, pMap map[string][]string, cMap map[string]*CommentContent, depth int) { + childs, ok := pMap[pid] + if !ok { + return + } + + for _, child := range childs { + comment := cMap[child] + comment.Depth = depth + *comments = append(*comments, comment) + // findCommentChilds + findCommentChilds(comments, child, pMap, cMap, depth+1) + } +} + +// CommentLike indicates that a user like/dislike a Comment +func CommentLike(c echo.Context) error { + postID := c.FormValue("id") + if postID == "" { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := session.Get(c) + + // check whether you already liked this comment + status, err := commentLikeStatus(postID, sess.ID) + if err != nil { + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + if status == OpCommentLike { + err = commentLike(postID, sess.ID, OpCommentLike, OpDelete) + } else { + err = commentLike(postID, sess.ID, OpCommentLike, OpUpdate) + } + if err != nil { + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + var q *gocql.Query + if status == OpCommentLike { // from like to normal, -1 + q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes-1 WHERE id=?`, postID) + } else if status == OpCommentDislike { // from dislike to like , + 2 + q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes+2 WHERE id=?`, postID) + } else { // from normal to like, + 1 + q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes+1 WHERE id=?`, postID) + } + + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + } + + return c.JSON(http.StatusOK, misc.HTTPResp{}) +} + +// CommentDislike indicates that a user dislike a Comment +func CommentDislike(c echo.Context) error { + postID := c.FormValue("id") + if postID == "" { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := session.Get(c) + + // check whether you already liked this comment + status, err := commentLikeStatus(postID, sess.ID) + if err != nil { + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + if status == OpCommentDislike { + err = commentLike(postID, sess.ID, OpCommentDislike, OpDelete) + } else { + err = commentLike(postID, sess.ID, OpCommentDislike, OpUpdate) + } + + if err != nil { + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + var q *gocql.Query + if status == OpCommentLike { // from like to dislike, -2 + q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes-2 WHERE id=?`, postID) + } else if status == OpCommentDislike { // from dislike to normal + 1 + q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes+1 WHERE id=?`, postID) + } else { // from normal to dislike -1 + q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes-1 WHERE id=?`, postID) + } + + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + } + + return c.JSON(http.StatusOK, misc.HTTPResp{}) +} + +func commentLikeStatus(id string, uid string) (int, error) { + var tp int + q := misc.CQL.Query(`SELECT type FROM comment_like WHERE uid=? and comment_id=?`, uid, id) + err := q.Scan(&tp) + if err != nil { + if err.Error() == misc.CQLNotFound { + return 0, nil + } + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return 0, err + } + return tp, nil +} + +// postLike is the action that a user click like or dislike on a post/comment +func commentLike(id string, uid string, tp int, op int) error { + var q *gocql.Query + switch op { + case OpUpdate: + q = misc.CQL.Query(`UPDATE comment_like SET type=?,input_date=? WHERE uid=? and comment_id=?`, tp, time.Now().Unix(), uid, id) + case OpDelete: + q = misc.CQL.Query(`DELETE FROM comment_like WHERE uid=? and comment_id=?`, uid, id) + } + + err := q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) + return err + } + + return nil +} diff --git a/internal/post/const.go b/internal/post/const.go index bfccf642..f9988b0e 100644 --- a/internal/post/const.go +++ b/internal/post/const.go @@ -13,3 +13,24 @@ const ( // ArticleType means the post is an ariticle ArticleType = 1 ) + +const ( + // OpCommentLike means a user like a post + OpCommentLike = 1 + // OpCommentDislike means a user dislike a post + OpCommentDislike = 2 +) + +const ( + // OpUpdate is the update operation + OpUpdate = 1 + // OpDelete is the delete operation + OpDelete = 2 +) + +const ( + // StatusNormal means post is in normal status + StatusNormal = 0 + // StatusDeleted means post is deleted + StatusDeleted = 1 +) diff --git a/internal/post/modify.go b/internal/post/modify.go new file mode 100644 index 00000000..aba4b32d --- /dev/null +++ b/internal/post/modify.go @@ -0,0 +1,75 @@ +package post + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo" + "github.com/microcosm-cc/bluemonday" + "github.com/thinkindev/im.dev/internal/misc" + "github.com/thinkindev/im.dev/internal/session" + "github.com/thinkindev/im.dev/internal/utils" +) + +// Preview return the new review html of article +func Preview(c echo.Context) error { + render := c.FormValue("render") + + newr := modify(render) + return c.JSON(http.StatusOK, misc.HTTPResp{ + Data: newr, + }) +} + +/* modify the post content*/ + +// every user input need to be modified +// @user -> @user +// remove js,iframe such html tags and attributes +func modify(s string) string { + p := bluemonday.UGCPolicy() + p.AllowAttrs("class").Globally() + p.AllowAttrs("id").Globally() + p.AllowElements("input") + p.AllowAttrs("checked").OnElements("input") + p.AllowAttrs("disabled").OnElements("input") + p.AllowAttrs("type").OnElements("input") + p.AllowAttrs("style").OnElements("span") + p.AllowAttrs("style").OnElements("td") + p.AllowAttrs("style").OnElements("th") + // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines + render := p.Sanitize(s) + + afterRender := make([]rune, 0, len(render)) + idParseFlag := false + tempName := make([]rune, 0) + for _, r := range render { + if r == '@' { + idParseFlag = true + afterRender = append(afterRender, r) + continue + } + if idParseFlag { + if utils.ValidNameRune(r) { + tempName = append(tempName, r) + } else { + // end flag for parse name + idParseFlag = false + + // check name exist + if session.CheckUserExist(string(tempName)) { + // converse @name -> @user + afterRender = append(afterRender, []rune(fmt.Sprintf("%s", string(tempName), string(tempName)))...) + } else { + afterRender = append(afterRender, tempName...) + } + + afterRender = append(afterRender, r) + } + continue + } + + afterRender = append(afterRender, r) + } + return string(afterRender) +} diff --git a/internal/post/post.go b/internal/post/post.go index 3c7f241a..235520f8 100644 --- a/internal/post/post.go +++ b/internal/post/post.go @@ -1,67 +1 @@ package post - -import ( - "fmt" - "net/http" - - "github.com/labstack/echo" - "github.com/microcosm-cc/bluemonday" - "github.com/thinkindev/im.dev/internal/misc" - "github.com/thinkindev/im.dev/internal/session" - "github.com/thinkindev/im.dev/internal/utils" -) - -// Preview return the new review html of article -func Preview(c echo.Context) error { - render := c.FormValue("render") - - newr := modify(render) - return c.JSON(http.StatusOK, misc.HTTPResp{ - Data: newr, - }) -} - -/* modify the post content*/ - -// every user input need to be modified -// @user -> @user -// remove js,iframe such html tags and attributes -func modify(s string) string { - p := bluemonday.UGCPolicy() - p.AllowAttrs("class").Globally() - // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines - render := p.Sanitize(s) - - afterRender := make([]rune, 0, len(render)) - idParseFlag := false - tempName := make([]rune, 0) - for _, r := range render { - if r == '@' { - idParseFlag = true - afterRender = append(afterRender, r) - continue - } - if idParseFlag { - if utils.ValidNameRune(r) { - tempName = append(tempName, r) - } else { - // end flag for parse name - idParseFlag = false - - // check name exist - if session.CheckUserExist(string(tempName)) { - // converse @name -> @user - afterRender = append(afterRender, []rune(fmt.Sprintf("%s", string(tempName), string(tempName)))...) - } else { - afterRender = append(afterRender, tempName...) - } - - afterRender = append(afterRender, r) - } - continue - } - - afterRender = append(afterRender, r) - } - return string(afterRender) -} diff --git a/internal/utils/time.go b/internal/utils/time.go index a460a67a..7e0c731d 100644 --- a/internal/utils/time.go +++ b/internal/utils/time.go @@ -18,15 +18,15 @@ func Time2ReadableString(t time.Time) string { now := time.Now().Local() intv := now.Unix() - t.Unix() if intv < 60 { - return strconv.FormatInt(intv, 10) + "seconds ago" + return strconv.FormatInt(intv, 10) + " seconds ago" } if intv < 3600 { - return strconv.FormatInt(intv/60, 10) + "minutes ago" + return strconv.FormatInt(intv/60, 10) + " minutes ago" } if intv < 86400 { - return strconv.FormatInt(intv/3600, 10) + "hours ago" + return strconv.FormatInt(intv/3600, 10) + " hours ago" } y1, m1, d1 := now.Date() diff --git a/quick-start/cql/start.cql b/quick-start/cql/start.cql index df3ba4d3..382fb9ea 100644 --- a/quick-start/cql/start.cql +++ b/quick-start/cql/start.cql @@ -51,7 +51,8 @@ CREATE TABLE IF NOT EXISTS tags ( CREATE TABLE IF NOT EXISTS comment ( id text, - uid text, -- author user id + pid text, -- parent comment id + uid text, -- author user id post_id text, -- related post id post_type tinyint, -- related post type @@ -60,11 +61,16 @@ CREATE TABLE IF NOT EXISTS comment ( render text, -- rendered html publish_date bigint, + edit_date bigint, + + status tinyint, -- 0: normal, 1: deleted PRIMARY KEY (id) ) WITH gc_grace_seconds = 10800; CREATE CUSTOM INDEX IF NOT EXISTS ON comment (post_id) USING 'org.apache.cassandra.index.sasi.SASIIndex' ; + + CREATE TABLE IF NOT EXISTS post_counter ( id text, -- post id @@ -73,4 +79,21 @@ CREATE TABLE IF NOT EXISTS post_counter ( recommands counter, PRIMARY KEY (id) -) WITH gc_grace_seconds = 10800; \ No newline at end of file +) WITH gc_grace_seconds = 10800; + +CREATE TABLE IF NOT EXISTS comment_counter ( + id text, -- comment id + likes counter, + PRIMARY KEY (id) +) WITH gc_grace_seconds = 10800; + +-- record whether a user likes a comment +CREATE TABLE IF NOT EXISTS comment_like ( + comment_id text, -- comment id + uid text, -- user id + type tinyint, -- 1: like 2: dislike + input_date bigint, + PRIMARY KEY (comment_id,uid) +) WITH gc_grace_seconds = 10800; +CREATE CUSTOM INDEX IF NOT EXISTS ON comment_like (uid) + USING 'org.apache.cassandra.index.sasi.SASIIndex' ; diff --git a/ui/src/assets/icon/iconfont.css b/ui/src/assets/icon/iconfont.css new file mode 100644 index 00000000..69f13563 --- /dev/null +++ b/ui/src/assets/icon/iconfont.css @@ -0,0 +1,37 @@ +@font-face {font-family: "iconfont"; + src: url('iconfont.eot?t=1568170482672'); /* IE9 */ + src: url('iconfont.eot?t=1568170482672#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAPEAAsAAAAACEgAAAN1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDOAqDQIMIATYCJAMYCw4ABCAFhG0HZBs6BxEVnHnIfh7GtojtKjpzws2LWP1NWda478HzlIe9yZ+kKYRT11ZW9lFAunYSIKBu37nvH9BA+KhzQ/ntkUc7TIGy16YH1Uf/w/1Ml+L57NtcolqjEgc4HtC0Isl2gHgK4tl08D8Bu6EAP5Jkp8pXrF4fG40+SAGqe5dObbATSnRFpMJGYRXM1JFqFia2Mdk4D8z0/l69IW82GJiCvlftjhXaU9qNcQuK4zk0F4exw+kBfh4IkB1oUN0LjR2RwJmd4Kf6a4oC29AbMJQzv1vQ8xhcXvZWGJXBAYBgbOMfzwRloYcHAFAdLhPcGAuBgJsXgQFuPgQK3PwINLgFszABsAZMt4EBwEGKzxCr49cpvwD9gsiYOrDHrbtt6diJJw/a3bzZ8NatRjduNLh+3afHaeDVx1vaLFi6cX1kg/mL1m2IGiZrblYMOn49xVh9owLz10c3Cjx1q5EfteQBpH39G8zfGK38OX469e12u/HhGxVCY+eU7bpjSq97zY7drBhy/HqZLtun/hrVemOjWmXN/6P0kT+nFWOFO/7bJCtLtss2h7+No0d00OC7vdQsOXxYbN5Mjg4GQwNvFpKnFVmSrdbhIaVruq3I3+HSZuXKtUvXpkIz8O8eUGTB+mHDkoTD+x1HObrXVqWdQt4f0bqgSED3ejb2HL8QaPik59muFb5mV5G+NP5Fc6oXzd7VHQDPkSvigHdIBkhpL3SA8cYTf/2qh9Hhcr7/rYKLf7P9NQB3Voz5jB5ej3bEgU/Dasz6hnLtVKByNY6usrTCstAOnyZ0MDWyRQ5iNwA/QwNfzwthHg4lipyI6ZhtIiMw8CERBJsMtMZnBxN/8oOFTSnwI5tK8/0J1xdBaV8g03gCihA7wSCIQyCEuIDW+AdgEuUdWIQkwCt8F75i6TFdlIu4MO6w/oPeKAhTw4Uf+Y4yuyXGOR7hG2PSFhjaPu15w4BxiDHpR47MAkQkD69kP3SO4IhkcONWMR/nrhNFX2o38qfCkwgtGNqB1T+gbUggnNKsyPz+HZIyZxErCqrU31CU6MrBoNU3QL8JQ6OCS7kn+SGNGBOAEBEPvBJ/5DikCY/ibgbasJbqYD+cdUYx0VTYTi/2d7kM8KOfY2UoUVqZylI+XB92uu1HOWWqdvFgj4FTsziuu+olMOUpqSXIMuH408vpBAAAAA==') format('woff2'), + url('iconfont.woff?t=1568170482672') format('woff'), + url('iconfont.ttf?t=1568170482672') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ + url('iconfont.svg?t=1568170482672#iconfont') format('svg'); /* iOS 4.1- */ +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-undo:before { + content: "\e633"; +} + +.icon-sousuo:before { + content: "\e632"; +} + +.icon-comments-alt:before { + content: "\e618"; +} + +.icon-jiantou_shang:before { + content: "\e634"; +} + +.icon-jiantou_xia:before { + content: "\e636"; +} + diff --git a/ui/src/assets/icon/iconfont.eot b/ui/src/assets/icon/iconfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..35cb862d1d763c0d3d63bc48128522acb8c1a31c GIT binary patch literal 2288 zcmd^B-D?zA6hCKX_QTm^8xwao6pd?RHqh!uH|c6qyQZc!rda%l4W$S&?#_OM-JNB3 zRw9is{e(V9llS72h%bo+rSzf2q)_NXYc2Gpg-Q#u1gucRLaDmj-<_G&n))B~4(HDA z{LVS|`_7p^{6q&JOcIGAkw>-xn0XfH;9*C9RNFMv)cE!KBespYs6c5ls7zVAHem?T zQPL?#bt;lZRWMFcnSI(^yzd-t6cv6$|CgNA)n`pr?dKpuT2xlSD-KFbuAOVK6wMaze19E5dJCo z9`F~Ct@)BU+xt#y6}~@0?=2c>jnDB$#Gi*AENQb9?x7c8?}Z*MYb8B(ta*mWgUL#z zicvGoi%Y)|`EJ6V5o4FFmPN-Et97?^SF!Bb!1S*J{tPwn}W0*YD;?_ z1Q2h;o>45>g6em-3qyeBrR`W3QN$*yc!*j;MGUr-5@d z*o2kAFj{q3)Jllw$wPkWBk5bPA3JZpMF~diqca2BW94x6)eMa_dK*b+{uujva1dic z5nVkLOJJ-G1hfYtX0#0g7cROnS+F8{8{U{Dc!frx`|)7z=xL;pY+>#|2?xO{dK4ZV zKr;LoQRsoi4FB&%tO+i;y9`*B2F|n)vHp#OBlg-314G36H%1&0d(%idV(%Hz)ASad zr?2dr1<#fx|A1Xzhb>J?1EF;sQo5zTwKtnzHZ8XJ?EBV5KJ0A*T7Lx|+_!seU6t$d z7;bxn`iTbOJecBLeH;ne0BVqZee8*)IJ}v|L5>ab&x@;yvbwk=>ZOGhMOj&x7d4+% z<WwG8)*}#p)ru7Bh8k*#>d?cs=^tf)3YX0HHqK%Udv*{t%cg zP$R>UaB|o4!{Fd3kMU&k!E$nv@#YQc1%5j@xnfDBBh6(n_b#X#c+9r)vI~+;;a==i zeRDkdnRTQjSrgINA#^YIrs$U!Ic!A%j0 z(oHw)`zCM*BJQ53{m>jNk&iaHSO&k<#R@RKIkvwCdpY7_FZjJKR_QsKaB+ZokPdLW z6`U1%mOgMXBOzRJv4li&)x|RS?_8_^|Kef~Qp;@@d#Q_M7ppXlZ?@BZfMQ%w$E&(# z>Y3=t)6qiOC})kbskXsUJy$Pk)wbGxPwUlM!6-*}#&@)-3B9aW+wIg&cyfFtyGN}J#EJGrdb&s8WJ4|KAtv8_=t^T+B&kLi8@13I!UKdug#(Af0)NJ z>RSJ``jfNmTCWL=p={^nwd1MLDdg`2Mrk6iM|rnaNU4`IhPP(aYjvXoYgy9E cW^KDxG&^SsTG=$}Cu(`EoC~zI*@8y&4_az>f&c&j literal 0 HcmV?d00001 diff --git a/ui/src/assets/icon/iconfont.svg b/ui/src/assets/icon/iconfont.svg new file mode 100644 index 00000000..4e529708 --- /dev/null +++ b/ui/src/assets/icon/iconfont.svg @@ -0,0 +1,41 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/icon/iconfont.ttf b/ui/src/assets/icon/iconfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1df09f3ec44bfd62faf3a5b0b509c0b55ded7289 GIT binary patch literal 2120 zcmd^A-D@0G6hCKX_M^MWnkLyzkTlttW(%5b(oNjfSku^QVx-cK)F2Wl)7{yRu)DMD z&XxpI7(c;>V)8D1Dy1)pLW=mHG!XKI{*51O9qOQ4GK z_3iq$VhZMw=C&BHmD|I$^G_W8gZzpm`02_!TLOqDaxW>SY{K=DBv>?2$xR2bFQN(7 zhaXX0xM(8oqiz}?ere+5Ib_;}?Fi?xX%(E~Ats^>hEdgK(JMh-A{Tk352bIxeq=xS z9>o~B61nWr5qh+pPQ9F@@oHx^Zm%D6e-{d8^o z0X(rv@CuE>_u_}OW2BL4ypFX4#cYJA7*QyifMnW$_K{ya5r}ZEC4sao^EzU z#yHf&p#Vn?@Xsroin6(~D*Dys4Mo{lUJ^ZD_Q}D0-WRRD39*K5yrEcH-Wc_FwXpA$ z)r*xnx2#>9KHiJ*dNJT$Od#0J$G8`h=?jACh1)UQ5sDvvb{HIl@;Fb$ch=)mj3>AI zuJK#(sSQ)gpQ){bd2r3w&f`{;*BubA33;yB=UL>b&&)G<$%Mb?@ipf}4XDMAi#wq- zN%zDFW=VF3Ixvd^9C?b@Zr$pOjmbyGVtut_&#d%yZL795l2nxBh}0GNSMIjR?Qd<1 zN5pbEI24m!vvMDPgIxZ&XZCpxaKKL8xhqo9TIr5;-vkcA#N89U7oLqJ^3Xm9%i#Ar zSOLa2$I`p-E=L^f27k=KK6-{G9qgwzD&fqrg0n(T)B6r)YN1aYEKz`NI#>q(or4wN zUmWbB5Z!mMn_5_Qu#aZ&&9>|NDZ*LbiIS!oS~7fLA)HOKwY9TbfRjQ z4pEeb9?P19s$eO1&!G%%+ytb|H=pb2M&8fKCM4pY&J&7n>L>L_KbtRg&|F8dQZkAYPg~bf=f3XuG$JHC|5mk z0f7}2#R8&2s$>OS0a@q{Y_iEt^2^NspZDf{^Ucd72YP!06o5me z0BEkxYQF#K)%E{}50ga$0Hp?9DuhOvgTVk@9tTb66+mqY*{sIIEXDDKDbTBj zS|36rij~vx37plqE~vFvL0v-`^Vun|F6I@~n;@nrUEnNW^SRJlfHijmfadSmyK+1+ zAxQ|&>j`5RkXT`5?Lh={i4nuM-a9M6yP`kyfm{QTo0zFL3Bvl zl$V}`24~GNfnY6%z%7=^dSs7=x`n63;_^*2>O-Dw%sT`@Vh}D*rhy)eU=oe5(cP#V z!NQmf|9Dh3fYalx>qY^EI*y)FJR^CoB{IeBs#EJAViVQc1{Nl8&nB)UC2Pgk&mNbzqE!arD2VnWIY6*H&s@mA-Kf^FWq zPa4vL9aIf;y@@4H-mS0QT<@xD80J5kV_WDXk-E4RND0{ML9L;T2zguby!_3Q2v~PV zOEYXeWjtq_oZo!w`@c}F)NU}+owVw{z;$haS0xbK@+s0X0A{5C}p!!9B+pXt8 z%cm;OmfK^iH55M#v6|-=TnaTP8*%Q&-#TbH$0FA^Tu zSvqXDN~4Qr`NIZs28mbRKEGAv1wU5tsk`J}Q;1iVclM3ylk$60eHIS!y-RuGH1WnI z`BQ4@EQ#sissD1*$GN_y6POuSzRT*rUh66Fc2KiJ`F4^2&@QpbRwV07Wt1Xcm1O0S z{Tm96L=oCpQh^o7WN9B?yPvh3e_Lhnv$9u7-lm1~BSe)hF2a=6?% z)|$!*1f%1sCV68v^Ah(6JJ+AJ45fy(%#(OQL3Bn@+$2l7dzta7lX|nAFJz~tk4R&_ zWhArLZ~RqlBr!K|!sRdW1a>i8n(g|x#@&HgMf;8qQTwF1KICtmq2%JI+wXTiE$=qF z;M{iVYSf?SGiwGF#Ye)QkeKOZ(R60Xq3}1%Pa|GaX8QXt+)=M`q386}yjBdT7){Lu z*BNG;WRTv6aaLM7(0NByGA*=oC^TYhR?qabD~tEFuEDE4>R70I-;^+1Z%CPxi9&zv zk6_+!987fy6uU;pq5NT}7B9 \ No newline at end of file diff --git a/ui/src/assets/icons/upvote.svg b/ui/src/assets/icons/upvote.svg new file mode 100644 index 00000000..c37c03e9 --- /dev/null +++ b/ui/src/assets/icons/upvote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/main.js b/ui/src/main.js index ae053204..52c8c36f 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -12,6 +12,8 @@ import "./theme/editor.css"; Vue.use(mavonEditor) +import './assets/icon/iconfont.css'; + import App from './App' Vue.use(ElementUI); diff --git a/ui/src/router/index.js b/ui/src/router/index.js index a5f02597..a58b4caa 100644 --- a/ui/src/router/index.js +++ b/ui/src/router/index.js @@ -14,7 +14,7 @@ const router = new Router({ component: Nav, children: [ { path: '/', meta: {'title':'im.dev'}, component: () => import('@/views/home')}, - { path: '/x/article/new', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')}, + { path: '/dev/article/new', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')}, { path: '/:uname/:arID', meta: {'title':'Article - im.dev',},component: () => import('@/views/article/detail')}, { path: '/:uname/:arID/edit', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')}, ] diff --git a/ui/src/theme/light/style.less b/ui/src/theme/light/style.less index 2ebbd7a4..64b2e272 100644 --- a/ui/src/theme/light/style.less +++ b/ui/src/theme/light/style.less @@ -5,4 +5,16 @@ .post-huge-title { font-size: 36px; -} \ No newline at end of file +} + +.meta-word { + font-size:12px; + color:rgb(124,124,124); +} + +.bold-meta-word { + color:rgb(135, 138, 140); + font-weight:700; + font-size:12px; +} + diff --git a/ui/src/theme/md_render.css b/ui/src/theme/md_render.css index 0a7d46c0..e5c9448e 100644 --- a/ui/src/theme/md_render.css +++ b/ui/src/theme/md_render.css @@ -1,5 +1,5 @@ .render .content p { - margin: 0 0 25px + margin: 0 0 8px } @@ -79,10 +79,10 @@ .render .content h4, .render .content h5, .render .content h6 { - margin: 0 0 15px; + margin: 0 0 12px; font-weight: 700; color: #2f2f2f; - line-height: 1.7; + line-height: 1.6; text-rendering: optimizelegibility } @@ -129,45 +129,7 @@ -.render .content ol { - list-style-type: square; -} -.render .content ul { - list-style-type: none; -} - -.render .content ol, -.render .content ul { - padding: 0; - margin-left: 16px; - margin-bottom: 20px; - display: block; -} - -.render .content ol { - margin-left: 36px; -} - -.render .content ol li, -.render .content ul li { - margin-bottom: 10px; - /* line-height: 30px */ -} - -.render .content ol li ol, -.render .content ol li ul, -.render .content ul li ol, -.render .content ul li ul { - margin-top: 15px -} - -.render .content ul li:before { - font-size: 19.8px; - padding-top: 4px; - padding-right: 12px; - content: '\2022'; -} .render .content ol .image-package, .render .content ul .image-package { @@ -418,3 +380,6 @@ } + + + diff --git a/ui/src/theme/style.less b/ui/src/theme/style.less index cfb29833..806877fa 100644 --- a/ui/src/theme/style.less +++ b/ui/src/theme/style.less @@ -60,4 +60,8 @@ input::-webkit-input-placeholder { .background-light-grey { background-color: #fafafa!important +} + +.el-message.el-message--error.network-error { + top: 20px !important; } \ No newline at end of file diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js index 1e5f0e6b..36416a0c 100644 --- a/ui/src/utils/request.js +++ b/ui/src/utils/request.js @@ -31,6 +31,14 @@ service.interceptors.response.use( }, error => { var response = error.response + if (response == undefined) { + Message.error({ + message: error.message, + duration: 3000, + customClass: 'network-error' + }) + return Promise.reject(error) + } if (response.data.err_code == 1001) { store.dispatch("setNeedSignin", 1) store.dispatch("ClearUserInfo") @@ -48,10 +56,7 @@ service.interceptors.response.use( return Promise.reject(response.data.message+' : '+ response.data.err_code) } - Message.error({ - content: error.message, - duration: 3 - }) + Message.error(error.message) return Promise.reject(error) }) diff --git a/ui/src/views/article/detail.vue b/ui/src/views/article/detail.vue index 0b8d954b..6d406926 100644 --- a/ui/src/views/article/detail.vue +++ b/ui/src/views/article/detail.vue @@ -50,8 +50,8 @@
{{arDetail.title}}
@@ -62,10 +62,10 @@ - + @@ -111,7 +111,6 @@ export default { article_id: this.arID } }).then(res0 => { - console.log(222) request({ url: "/web/user/get", method: "GET", @@ -137,7 +136,6 @@ export default { .square { margin-top: 3px; line-height: 36px; - border: 1px solid white; text-align: center; z-index: 100; font-size: 25px; diff --git a/ui/src/views/article/edit.vue b/ui/src/views/article/edit.vue index 4a71450c..45d69d23 100644 --- a/ui/src/views/article/edit.vue +++ b/ui/src/views/article/edit.vue @@ -33,7 +33,7 @@
- + diff --git a/ui/src/views/components/discuss.vue b/ui/src/views/components/discuss.vue index 78140d1b..fd8fba3b 100644 --- a/ui/src/views/components/discuss.vue +++ b/ui/src/views/components/discuss.vue @@ -1,27 +1,73 @@ @@ -30,7 +76,7 @@ import editor from "./editor" import render from "./render" import request from "@/utils/request"; export default { - props: ['postID','postType'], + props: ['postAuthorID','postID','postType'], components: {editor,render}, data() { return { @@ -41,16 +87,229 @@ export default { tempCommentRender: '', commentPreviewd : false, - comments: [] + comments: [], + currentCommentID: '', + + tempReply: { + md:'', + render: '' + }, + replyPreviewd : false, + tempReplyRender: '', + + currentEditCommentID: '', + tempEditReply: { + md: '', + render: '' + } }; }, watch: { + "$store.state.user.id"() { + if (this.$store.state.user.id != '') { + this.initComments() + } + }, }, methods: { + revertComment(id) { + request({ + url: "/web/comment/revert", + method: "POST", + params: { + id: id + } + }).then(res => { + for (var i=0;i { + request({ + url: "/web/comment/delete", + method: "POST", + params: { + id: id + } + }).then(res => { + for (var i=0;i { + }); + }, + upvoteComment(c) { + request({ + url: "/web/comment/like", + method: "POST", + params: { + id: c.id + } + }).then(res => { + if (c.liked == 0) { + c.liked = 1 + c.likes++ + } else if (c.liked == 1) { + c.liked = 0 + c.likes -- + } else { + c.liked = 1 + c.likes = c.likes + 2 + } + }); + }, + downvoteComment(c) { + request({ + url: "/web/comment/dislike", + method: "POST", + params: { + id: c.id + } + }).then(res => { + if (c.liked == 0) { + c.liked = 2 + c.likes-- + } else if (c.liked == 1) { + c.liked = 2 + c.likes = c.likes - 2 + } else { + c.liked = 0 + c.likes ++ + } + }); + }, + previewReply() { + this.replyPreviewd = true + request({ + url: "/web/post/preview", + method: "POST", + params: { + render: this.tempReplyRender + } + }).then(res => { + this.tempReply.render = res.data.data + }); + }, + editReplySetMD(md,render) { + this.tempEditReply.md = md + this.tempEditReply.render = render + }, + replySetMD(md,render) { + this.tempReply.md = md + this.tempReplyRender = render + }, + cancelEditReply() { + for (var i=0;i { + this.currentEditCommentID = '' + this.tempEditReply.md = '' + }).catch(() => { + }); + } else { + this.currentEditCommentID = '' + this.tempEditReply.md = '' + } + break + } + } + }, + editReply(c) { + this.currentEditCommentID = c.id + this.currentCommentID = '' + this.tempEditReply = { + md: c.md, + render: '' + } + }, + reply(id) { + if (id == this.currentCommentID) { + this.currentCommentID = '' + return + } + this.currentCommentID = id + this.currentEditCommentID = '' + }, + publishEditReply(id) { + request({ + url: "/web/comment/edit", + method: "POST", + params: { + id: id, + content: JSON.stringify(this.tempEditReply) + } + }).then(res => { + this.currentEditCommentID = '' + for (var i=0;i { + this.tempReply = { + md : '', + render: '' + } + this.tempReplyRender = '' + this.replyPreviewd = false + this.currentCommentID = '' + var newComments = [] + for (var i=0;i { + this.comments = res.data.data + }); } }, mounted() { - request({ - url: "/web/post/queryComments", - method: "GET", - params: { - post_id: this.postID, - } - }).then(res => { - this.comments = res.data.data - console.log(this.comments) - }); + this.initComments() } }; + + \ No newline at end of file diff --git a/ui/src/views/components/editor.vue b/ui/src/views/components/editor.vue index 612e63ca..ea38f075 100644 --- a/ui/src/views/components/editor.vue +++ b/ui/src/views/components/editor.vue @@ -1,26 +1,26 @@