fix #27, add discuss component

pull/35/head
sunface 6 years ago
parent ce097358ca
commit 90d1f4bb2e

@ -20,6 +20,12 @@ func apiHandler(e *echo.Echo) {
e.POST("/web/article/saveChanges", post.SaveArticleChanges, session.CheckSignIn) e.POST("/web/article/saveChanges", post.SaveArticleChanges, session.CheckSignIn)
// comment apis // comment apis
e.POST("/web/post/comment", post.Comment, session.CheckSignIn) e.POST("/web/comment/create", post.Comment, session.CheckSignIn)
e.GET("/web/post/queryComments", post.QueryComments) 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)
} }

@ -23,3 +23,9 @@ const (
PostNotFound = 1101 PostNotFound = 1101
PostNotFoundMsg = "Target post not found" PostNotFoundMsg = "Target post not found"
) )
// comment
const (
CommentLiked = 1200
CommentLikedMsg = "You have agreed this comment before"
)

@ -2,11 +2,14 @@ package post
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gocql/gocql"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/thinkindev/im.dev/internal/ecode" "github.com/thinkindev/im.dev/internal/ecode"
"github.com/thinkindev/im.dev/internal/misc" "github.com/thinkindev/im.dev/internal/misc"
@ -18,14 +21,23 @@ import (
// CommentContent holds the comment content // CommentContent holds the comment content
type CommentContent struct { type CommentContent struct {
ID string `json:"id"` ID string `json:"id"`
PID string `json:"pid"`
Depth int `json:"depth"`
MD string `json:"md"` MD string `json:"md"`
Render string `json:"render"` Render string `json:"render"`
UID string `json:"uid"` UID string `json:"uid"`
UName string `json:"uname"` UName string `json:"uname"`
UNickname string `json:"unickname"` UNickname string `json:"unickname"`
UAvatar string `json:"uavatar"`
PublishDate string `json:"date"` PublishDate string `json:"date"`
publishDate int64 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 // 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] a[i], a[j] = a[j], a[i]
} }
func (a Comments) Less(i, j int) bool { // 重写 Less() 方法, 从小到大排序 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 // 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 cc.UID = sess.ID
// generate id for article // generate id for article
cc.ID = misc.GenID() cc.ID = misc.GenID()
cc.UName = sess.Name
cc.UNickname = sess.NickName
// modify render // modify render
cc.Render = modify(cc.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 (?,?,?,?,?,?,?)", 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 { if err != nil {
misc.Log.Warn("access database error", zap.Error(err)) misc.Log.Warn("access database error", zap.Error(err))
return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ 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 // QueryComments return the comments by post id
func QueryComments(c echo.Context) error { func QueryComments(c echo.Context) error {
postID := c.FormValue("post_id") postID := c.FormValue("post_id")
@ -126,33 +385,137 @@ 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() iter := q.Iter()
comments := make(Comments, 0) // comment map
var cid, uid, md, render string cMap := make(map[string]*CommentContent)
var pdate int64 // parent -> child map
for iter.Scan(&cid, &uid, &md, &render, &pdate) { 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{ comment := &CommentContent{
ID: cid, ID: cid,
PID: pid,
UID: uid, UID: uid,
MD: md, MD: md,
Render: render, Render: render,
publishDate: pdate, publishDate: pdate,
editDate: edate,
Status: status,
} }
u := session.GetUserByID(comment.UID) u := session.GetUserByID(comment.UID)
if u == nil { if u == nil {
continue continue
} }
if status == StatusDeleted {
comment.MD = "[deleted]"
comment.Render = "[deleted]"
comment.UName = "[deleted]"
comment.UNickname = "[deleted]"
} else {
comment.UName = u.Name comment.UName = u.Name
comment.UNickname = u.NickName comment.UNickname = u.NickName
comment.UAvatar = u.Avatar }
comment.PublishDate = utils.Time2ReadableString(time.Unix(comment.publishDate, 0)) 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)
}
}
}
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,
})
}
// 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) 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,
})
} }
sort.Sort(comments) 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 { if err := iter.Close(); err != nil {
misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String())) misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
@ -161,8 +524,165 @@ func QueryComments(c echo.Context) error {
Message: ecode.CommonErrorMsg, 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{ return c.JSON(http.StatusOK, misc.HTTPResp{
Data: comments, 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
}

@ -13,3 +13,24 @@ const (
// ArticleType means the post is an ariticle // ArticleType means the post is an ariticle
ArticleType = 1 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
)

@ -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 -> <a href="UserPage">@user</a>
// 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 -> <a href="UserPage">@user</a>
afterRender = append(afterRender, []rune(fmt.Sprintf("<a href='http://localhost:9532/%s'>%s</a>", string(tempName), string(tempName)))...)
} else {
afterRender = append(afterRender, tempName...)
}
afterRender = append(afterRender, r)
}
continue
}
afterRender = append(afterRender, r)
}
return string(afterRender)
}

@ -1,67 +1 @@
package post 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 -> <a href="UserPage">@user</a>
// 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 -> <a href="UserPage">@user</a>
afterRender = append(afterRender, []rune(fmt.Sprintf("<a href='http://localhost:9532/%s'>%s</a>", string(tempName), string(tempName)))...)
} else {
afterRender = append(afterRender, tempName...)
}
afterRender = append(afterRender, r)
}
continue
}
afterRender = append(afterRender, r)
}
return string(afterRender)
}

@ -51,6 +51,7 @@ CREATE TABLE IF NOT EXISTS tags (
CREATE TABLE IF NOT EXISTS comment ( CREATE TABLE IF NOT EXISTS comment (
id text, id text,
pid text, -- parent comment id
uid text, -- author user id uid text, -- author user id
post_id text, -- related post id post_id text, -- related post id
@ -60,11 +61,16 @@ CREATE TABLE IF NOT EXISTS comment (
render text, -- rendered html render text, -- rendered html
publish_date bigint, publish_date bigint,
edit_date bigint,
status tinyint, -- 0: normal, 1: deleted
PRIMARY KEY (id) PRIMARY KEY (id)
) WITH gc_grace_seconds = 10800; ) WITH gc_grace_seconds = 10800;
CREATE CUSTOM INDEX IF NOT EXISTS ON comment (post_id) CREATE CUSTOM INDEX IF NOT EXISTS ON comment (post_id)
USING 'org.apache.cassandra.index.sasi.SASIIndex' ; USING 'org.apache.cassandra.index.sasi.SASIIndex' ;
CREATE TABLE IF NOT EXISTS post_counter ( CREATE TABLE IF NOT EXISTS post_counter (
id text, -- post id id text, -- post id
@ -74,3 +80,20 @@ CREATE TABLE IF NOT EXISTS post_counter (
PRIMARY KEY (id) PRIMARY KEY (id)
) WITH gc_grace_seconds = 10800; ) 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' ;

@ -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";
}

Binary file not shown.

@ -0,0 +1,41 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="iconfont" horiz-adv-x="1024" >
<font-face
font-family="iconfont"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="undo" unicode="&#58931;" d="M761.856-128c113.728 206.048 132.896 520.32-313.856 509.824l0-253.824-384 384 384 384 0-248.384c534.976 13.952 594.56-472.224 313.856-775.616z" horiz-adv-x="1024" />
<glyph glyph-name="sousuo" unicode="&#58930;" d="M474.453333 11.946667c-225.28 0-409.6 184.32-409.6 409.6s184.32 409.6 409.6 409.6 409.6-184.32 409.6-409.6-184.32-409.6-409.6-409.6z m0 68.266666c187.733333 0 341.333333 153.6 341.333334 341.333334s-153.6 341.333333-341.333334 341.333333-341.333333-153.6-341.333333-341.333333 153.6-341.333333 341.333333-341.333334z m252.586667-54.613333c-13.653333 13.653333-10.24 37.546667 3.413333 47.786667s37.546667 10.24 47.786667-3.413334l64.853333-78.506666c13.653333-13.653333 10.24-37.546667-3.413333-47.786667s-37.546667-10.24-47.786667 3.413333l-64.853333 78.506667z" horiz-adv-x="1024" />
<glyph glyph-name="comments-alt" unicode="&#58904;" d="M831.914144 448.04623V768.013209c0 70.592715-57.394077 127.986791-127.986792 127.986791H127.986791C57.394077 896 0 838.605923 0 768.013209v-319.966979c0-70.592715 57.394077-127.986791 127.986791-127.986791v-108.388814c0-15.998349 18.198122-25.1974 30.996801-15.59839l165.582912 124.187183H703.927352c70.592715-0.199979 127.986791 57.194097 127.986792 127.786812z m191.980187 127.986792h-127.986791v-127.986792c0-105.789082-86.191105-191.980187-191.980188-191.980187H383.960374v-127.986791c0-70.592715 57.394077-127.986791 127.986791-127.986792h251.374058l165.582911-124.187183c12.798679-9.599009 30.996801-0.399959 30.996801 15.59839V0.09246h63.993396c70.592715 0 127.986791 57.394077 127.986791 127.986792V448.04623c0 70.592715-57.394077 127.986791-127.986791 127.986792z" horiz-adv-x="1152" />
<glyph glyph-name="jiantou_shang" unicode="&#58932;" d="M434.666714 708.530676C297.43919 528.723334 160.191798 348.917799 22.964274 169.110458c-48.854624-64.034092-3.20604-156.297589 77.324255-156.297589h823.421135c80.530295 0 126.198748 92.263496 77.324256 156.297589C863.806396 348.917799 726.559004 528.723334 589.333286 708.530676c-38.933061 51.01125-115.733512 51.01125-154.666572 0z" horiz-adv-x="1024" />
<glyph glyph-name="jiantou_xia" unicode="&#58934;" d="M589.320643 59.487386c137.245586 179.809148 274.469497 359.614683 411.693408 539.422024 48.874492 64.014224 3.225908 156.297589-77.322449 156.297589H100.306592c-80.548357 0-126.195135-92.283365-77.342318-156.297589 137.24378-179.807341 274.467691-359.612876 411.713277-539.422024 38.933061-51.009444 115.711837-51.009444 154.643092 0z" horiz-adv-x="1024" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Binary file not shown.

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="rgb(135, 138, 140)"><path d="M757.76 637.44l-218.88 245.76c-14.72 16.64-40.32 16.64-54.4 0L265.6 637.44C244.48 613.76 261.12 576 293.12 576l437.76 0C762.24 576 779.52 613.76 757.76 637.44z" /></svg>

After

Width:  |  Height:  |  Size: 462 B

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="rgb(135, 138, 140)"><path d="M758.4 385.92 539.52 140.16c-14.72-16.64-40.32-16.64-54.4 0L266.24 385.92C244.48 409.6 261.76 448 293.12 448l437.76 0C762.88 448 779.52 409.6 758.4 385.92z" /></svg>

After

Width:  |  Height:  |  Size: 458 B

@ -12,6 +12,8 @@ import "./theme/editor.css";
Vue.use(mavonEditor) Vue.use(mavonEditor)
import './assets/icon/iconfont.css';
import App from './App' import App from './App'
Vue.use(ElementUI); Vue.use(ElementUI);

@ -14,7 +14,7 @@ const router = new Router({
component: Nav, component: Nav,
children: [ children: [
{ path: '/', meta: {'title':'im.dev'}, component: () => import('@/views/home')}, { 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', meta: {'title':'Article - im.dev',},component: () => import('@/views/article/detail')},
{ path: '/:uname/:arID/edit', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')}, { path: '/:uname/:arID/edit', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')},
] ]

@ -6,3 +6,15 @@
.post-huge-title { .post-huge-title {
font-size: 36px; font-size: 36px;
} }
.meta-word {
font-size:12px;
color:rgb(124,124,124);
}
.bold-meta-word {
color:rgb(135, 138, 140);
font-weight:700;
font-size:12px;
}

@ -1,5 +1,5 @@
.render .content p { .render .content p {
margin: 0 0 25px margin: 0 0 8px
} }
@ -79,10 +79,10 @@
.render .content h4, .render .content h4,
.render .content h5, .render .content h5,
.render .content h6 { .render .content h6 {
margin: 0 0 15px; margin: 0 0 12px;
font-weight: 700; font-weight: 700;
color: #2f2f2f; color: #2f2f2f;
line-height: 1.7; line-height: 1.6;
text-rendering: optimizelegibility 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 ol .image-package,
.render .content ul .image-package { .render .content ul .image-package {
@ -418,3 +380,6 @@
} }

@ -61,3 +61,7 @@ input::-webkit-input-placeholder {
.background-light-grey { .background-light-grey {
background-color: #fafafa!important background-color: #fafafa!important
} }
.el-message.el-message--error.network-error {
top: 20px !important;
}

@ -31,6 +31,14 @@ service.interceptors.response.use(
}, },
error => { error => {
var response = error.response 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) { if (response.data.err_code == 1001) {
store.dispatch("setNeedSignin", 1) store.dispatch("setNeedSignin", 1)
store.dispatch("ClearUserInfo") store.dispatch("ClearUserInfo")
@ -48,10 +56,7 @@ service.interceptors.response.use(
return Promise.reject(response.data.message+' : '+ response.data.err_code) return Promise.reject(response.data.message+' : '+ response.data.err_code)
} }
Message.error({ Message.error(error.message)
content: error.message,
duration: 3
})
return Promise.reject(error) return Promise.reject(error)
}) })

@ -50,8 +50,8 @@
<el-col <el-col
:xs="{span:22,offset:2}" :xs="{span:22,offset:2}"
:sm="{span:17,offset:3}" :sm="{span:17,offset:3}"
:md="{span: 12,offset:6}" :md="{span: 13,offset:6}"
:lg="{ span: 12, offset: 6 }" :lg="{ span: 13, offset: 6 }"
> >
<div class="post-huge-title margin-top-30">{{arDetail.title}}</div> <div class="post-huge-title margin-top-30">{{arDetail.title}}</div>
<render :content="arDetail.render"></render> <render :content="arDetail.render"></render>
@ -62,10 +62,10 @@
<el-col <el-col
:xs="{span:22,offset:2}" :xs="{span:22,offset:2}"
:sm="{span:17,offset:3}" :sm="{span:17,offset:3}"
:md="{span: 12,offset:6}" :md="{span: 13,offset:6}"
:lg="{ span: 12, offset: 6 }" :lg="{ span: 13, offset: 6 }"
> >
<discuss id="discuss" :postID="this.arID" postType="1"></discuss> <discuss id="discuss" :postID="this.arID" postType="1" :postAuthorID="authorInfo.id"></discuss>
</el-col> </el-col>
</el-row> </el-row>
@ -111,7 +111,6 @@ export default {
article_id: this.arID article_id: this.arID
} }
}).then(res0 => { }).then(res0 => {
console.log(222)
request({ request({
url: "/web/user/get", url: "/web/user/get",
method: "GET", method: "GET",
@ -137,7 +136,6 @@ export default {
.square { .square {
margin-top: 3px; margin-top: 3px;
line-height: 36px; line-height: 36px;
border: 1px solid white;
text-align: center; text-align: center;
z-index: 100; z-index: 100;
font-size: 25px; font-size: 25px;

@ -33,7 +33,7 @@
<editor :editorHeight="editorHeight" class="margin-top-5" parent="article" :md="tempArticle.md" @articleSetMD="articleSetMD"></editor> <editor :editorHeight="editorHeight" class="margin-top-5" parent="article" :md="tempArticle.md" @articleSetMD="articleSetMD"></editor>
</el-col> </el-col>
<el-col :span="12" v-if="previewReset" class="margin-top-5" style="border:1px solid #eee;border-bottom:none;border-right:none;"> <el-col :span="12" v-if="previewReset" class="margin-top-5" style="border:1px solid #eee;border-bottom:none;border-right:none;">
<render id ="render-content" :content="tempArticle.render" :style="{'height':editorHeight,'overflow-y':'scroll'}" ></render> <render id ="render-content" :content="tempArticle.render" :style="{'height':editorHeight,'overflow-y':'scroll'}" style="padding:10px"></render>
</el-col> </el-col>
</el-row> </el-row>

@ -1,28 +1,74 @@
<template> <template>
<div class="discuss" style="padding:20px"> <div class="discuss" style="padding:20px">
<div class="write-comment"> <!-- comment editor -->
<div class="write-comment" style="border-bottom:1px solid #eee">
<editor placeholder="Add to the discussion" editorHeight="200px" parent="discuss" :md="tempComment.md" @discussSetMD="discussSetMD" v-if="!commentPreviewd"></editor> <editor placeholder="Add to the discussion" editorHeight="200px" parent="discuss" :md="tempComment.md" @discussSetMD="discussSetMD" v-if="!commentPreviewd"></editor>
<render :content="tempComment.render" style="height:300px;overflow-y:auto;background:white;" v-else></render> <render paddingTop="46px" paddingLeft="20px" :content="tempComment.render" style="height:200px;overflow-y:auto;background:white;" v-else></render>
<div class="float-right margin-top-5"> <span style="position:relative;float:right;margin-top:-190px;z-index:1000;margin-right:20px">
<el-button size="medium" class="border-ellipse background-grey" @click="previewComment" v-if="!commentPreviewd">PREVIEW</el-button> <span class="bold-meta-word hover-cursor" @click="previewComment" v-if="!commentPreviewd">PREVIEW</span>
<el-button size="medium" class="border-ellipse background-grey" @click="commentPreviewd=false" v-else>MARKDOWN</el-button> <span class="bold-meta-word hover-cursor" @click="commentPreviewd=false" v-else>MARKDOWN</span>
<el-button type="primary" size="medium" class="border-ellipse" @click="publishComment">PUBLISH</el-button> <span class="bold-meta-word hover-cursor margin-left-10" @click="publishComment">PUBLISH</span>
</span>
</div> </div>
<div class="sorter">SORT BY <span>BEST</span></div>
<div class="comments" v-if="comments.length>0" style="padding-bottom:30px;">
<div class="comment margin-top-30" v-for="c in comments" :key="c.id" :style="{'margin-left':c.depth * 23 + 'px'}">
<i class="iconfont icon-jiantou_shang hover-cursor upvote" :class="{'vote-highlighted':c.liked==1}" @click="upvoteComment(c)"></i>
<i class="iconfont icon-jiantou_xia hover-cursor downvote" :class="{'vote-highlighted':c.liked==2}" @click="downvoteComment(c)"></i>
<div class="header">
<router-link class="uname" :to="`/${c.uname}`" v-if="c.status==0">{{c.unickname}}</router-link>
<span v-else>[deleted]</span>
<span class="margin-left-5 date-agree">{{c.likes}} agreed &nbsp;·&nbsp; {{c.date}} &nbsp; <i v-if="c.edit_date!=undefined">·&nbsp;edited {{c.edit_date}}</i></span>
</div>
<!-- edit reply editor -->
<div class="write-comment reply-comment margin-top-10" v-if="currentEditCommentID == c.id">
<editor editorHeight="150px" parent="discuss" :toolbarsShow="false" :md="tempEditReply.md" @discussSetMD="editReplySetMD"></editor>
</div> </div>
<!-- body and footer, hide when editing -->
<render :content="c.render" class="body" v-else></render>
<div class="footer">
<span v-if="currentEditCommentID != c.id">
<span class="hover-cursor" @click="reply(c.id)" v-if="c.status!=1">Reply</span>
<span class="hover-cursor margin-left-5 margin-right-10" v-if="$store.state.user.id==c.uid && c.status!=1" @click="editReply(c)">Edit</span>
<el-dropdown placement="bottom-start" v-if="$store.state.user.id==c.uid">
<span class="el-dropdown-link">
<i class="el-icon-more hover-cursor"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="deleteComment(c.id)" v-show="c.status!=1"><i class="el-icon-delete" ></i>Delete comment</el-dropdown-item>
<el-dropdown-item @click.native="revertComment(c.id)" v-show="c.status==1"><i class="iconfont icon-undo"></i>Revert comment</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div class="comments">
<div class="comment margin-top-40" v-for="c in comments" :key="c.id" style="border-left:2px solid #ccc;padding-left:10px;">
<div><i class="el-icon-caret-top" style="position:absolute;"></i></div>
<div><i class="el-icon-caret-bottom" style="position:absolute;top:26px;right:25px"></i></div>
<div>{{c.unickname}} {{c.date}}</div> <span v-if="currentCommentID == c.id" class="float-right">
<render :content="c.render"></render> <span class="hover-cursor" @click="previewReply" v-if="!replyPreviewd">Preview</span>
<div>reply</div> <span class="hover-cursor" @click="replyPreviewd=false" v-else>Markdown</span>
<span class=" hover-cursor margin-left-5" @click="publishReply(c.id)">Publish</span>
</span>
</span>
<span v-else class="float-right">
<span class=" hover-cursor margin-left-5" @click="cancelEditReply">Cancel</span>
<span class=" hover-cursor margin-left-5" @click="publishEditReply(c.id)">Publish</span>
</span>
</div>
<!-- reply editor -->
<div class="write-comment reply-comment margin-top-10" v-if="currentCommentID == c.id">
<editor placeholder="Add to the discussion" editorHeight="150px" parent="discuss" :toolbarsShow="false" :md="tempReply.md" @discussSetMD="replySetMD" v-if="!replyPreviewd"></editor>
<render :content="tempReply.render" style="height:150px;overflow-y:auto;background:white;" v-else></render>
</div> </div>
</div> </div>
</div> </div>
<div v-else style="text-align:center;padding-top:40px;padding-bottom:40px;">
<i class="iconfont icon-comments-alt" style="color:rgba(0, 121, 211, 0.4);font-size: 30px" />
<div class="meta-word margin-top-20" style="font-size:18px">No Comments Yet</div>
<div class="meta-word margin-top-15" style="font-size:15px">Be the first to share what you think!</div>
</div>
</div>
</template> </template>
<script> <script>
@ -30,7 +76,7 @@ import editor from "./editor"
import render from "./render" import render from "./render"
import request from "@/utils/request"; import request from "@/utils/request";
export default { export default {
props: ['postID','postType'], props: ['postAuthorID','postID','postType'],
components: {editor,render}, components: {editor,render},
data() { data() {
return { return {
@ -41,16 +87,229 @@ export default {
tempCommentRender: '', tempCommentRender: '',
commentPreviewd : false, commentPreviewd : false,
comments: [] comments: [],
currentCommentID: '',
tempReply: {
md:'',
render: ''
},
replyPreviewd : false,
tempReplyRender: '',
currentEditCommentID: '',
tempEditReply: {
md: '',
render: ''
}
}; };
}, },
watch: { watch: {
"$store.state.user.id"() {
if (this.$store.state.user.id != '') {
this.initComments()
}
},
}, },
methods: { methods: {
revertComment(id) {
request({
url: "/web/comment/revert",
method: "POST",
params: {
id: id
}
}).then(res => {
for (var i=0;i<this.comments.length;i++) {
if (this.comments[i].id == id) {
this.comments[i].md = res.data.data.md
this.comments[i].render = res.data.data.render
this.comments[i].uname = res.data.data.uname
this.comments[i].unickname = res.data.data.unickname
this.comments[i].status = 0
}
}
});
},
deleteComment(id) {
this.$confirm('Are you sure you want to delete your comment?', 'Delete comment', {
confirmButtonText: 'DELETE',
cancelButtonText: 'Cancel',
type: 'warning',
center: true
}).then(() => {
request({
url: "/web/comment/delete",
method: "POST",
params: {
id: id
}
}).then(res => {
for (var i=0;i<this.comments.length;i++) {
if (this.comments[i].id == id) {
this.comments[i].status= 1
this.comments[i].md = '[Deleted]'
this.comments[i].render = '[Deleted]'
}
}
});
}).catch(() => {
});
},
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.comments.length;i++) {
if (this.comments[i].id == this.currentEditCommentID) {
if (this.comments[i].md != this.tempEditReply.md) {
this.$confirm('Are you sure that you want to discard your changes?', 'Cancel Edit', {
confirmButtonText: 'Discard',
cancelButtonText: 'Keep',
type: 'warning',
center: true
}).then(() => {
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.comments.length;i++) {
if (this.comments[i].id == id) {
this.comments[i].md = this.tempEditReply.md
this.comments[i].render = this.tempEditReply.render
}
}
this.tempEditReply = {
md: '',
render: ''
}
});
},
publishReply(id) {
this.tempReply.render = this.tempReplyRender
request({
url: "/web/comment/reply",
method: "POST",
params: {
pid: id,
post_id: this.postID,
post_type: this.postType,
content: JSON.stringify(this.tempReply)
}
}).then(res => {
this.tempReply = {
md : '',
render: ''
}
this.tempReplyRender = ''
this.replyPreviewd = false
this.currentCommentID = ''
var newComments = []
for (var i=0;i<this.comments.length;i++) {
newComments.push(this.comments[i])
if (this.comments[i].id == res.data.data.pid) {
res.data.data.depth = this.comments[i].depth + 1
newComments.push(res.data.data)
}
}
this.comments = newComments
});
},
publishComment() { publishComment() {
this.tempComment.render = this.tempCommentRender this.tempComment.render = this.tempCommentRender
request({ request({
url: "/web/post/comment", url: "/web/comment/create",
method: "POST", method: "POST",
params: { params: {
post_id: this.postID, post_id: this.postID,
@ -64,6 +323,7 @@ export default {
} }
this.tempCommentRender = '' this.tempCommentRender = ''
this.commentPreviewd = false this.commentPreviewd = false
this.comments.unshift(res.data.data)
}); });
}, },
previewComment() { previewComment() {
@ -81,22 +341,94 @@ export default {
discussSetMD(md,render) { discussSetMD(md,render) {
this.tempComment.md = md this.tempComment.md = md
this.tempCommentRender = render this.tempCommentRender = render
}
}, },
mounted() { initComments() {
request({ request({
url: "/web/post/queryComments", url: "/web/comment/query",
method: "GET", method: "GET",
params: { params: {
post_id: this.postID, post_id: this.postID,
} }
}).then(res => { }).then(res => {
this.comments = res.data.data this.comments = res.data.data
console.log(this.comments)
}); });
} }
},
mounted() {
this.initComments()
}
}; };
</script> </script>
<style lang="less"> <style lang="less">
.discuss {
.v-note-wrapper {
min-height: 150px !important;
.v-note-op.shadow {
box-shadow: none;
border-bottom: 1px solid #efefef
}
textarea {
font-size:13px !important;
}
}
.reply-comment {
.v-note-wrapper {
border:1px solid #eee
}
}
}
</style>
<style lang="less" scoped>
.sorter {
color:rgb(124,124,124);
font-weight:700;
font-size:12px;
margin-top:30px;
padding-bottom:5px
}
.comments {
background:white;
padding:5px 25px;
margin-top:0px;
.comment {
i.vote-highlighted {
color: rgb(255, 69, 0);
}
.upvote {
position:absolute;
margin-left:-21px;
margin-top:-2px;
color: rgb(135, 138, 140);
font-size:10px;
}
.downvote {
position:absolute;
margin-left:-21px;
margin-top:9px;
color:rgb(135, 138, 140);
font-size:10px;
}
.header {
font-size:12px;
color:rgb(124,124,124);
.uname {
color:rgb(0, 121, 211);
text-decoration:none;
}
}
.body {
font-size:14px;
margin-top:7px
}
.footer {
color:rgb(135, 138, 140);
font-weight:700;
font-size:12px;
margin-top:10px
}
}
}
</style> </style>

@ -1,26 +1,26 @@
<template> <template>
<div class="editor"> <div class="editor">
<mavon-editor ref="areditor" :style="{height:editorHeight}" :language="$store.state.misc.lang" :value="md" :ishljs = "true" :toolbars="toolbars" :tabSize="2" @change="setMD" :subfield="false" @imgAdd="imgAdd" :placeholder="placeholder"></mavon-editor> <mavon-editor ref="areditor" :style="{height:editorHeight}" :language="$store.state.misc.lang" :value="md" :ishljs = "true" :toolbars="toolbars" :toolbarsFlag="toolbarsShow" :tabSize="2" @change="setMD" :subfield="false" @imgAdd="imgAdd" :placeholder="placeholder"></mavon-editor>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['md','parent','editorHeight',"placeholder"], props: ['md','parent','editorHeight',"placeholder","toolbarsShow"],
data() { data() {
return { return {
toolbars: { toolbars: {
bold: true, // bold: false, //
italic: true, // italic: true, //
header: true, // header: false, //
underline: true, // 线 underline: true, // 线
strikethrough: true, // 线 strikethrough: true, // 线
mark: true, // mark: true, //
superscript: true, // superscript: true, //
subscript: true, // subscript: true, //
quote: true, // quote: false, //
ol: true, // ol: false, //
ul: true, // ul: false, //
link: true, // link: true, //
imagelink: true, // imagelink: true, //
code: true, // code code: true, // code

@ -1,6 +1,6 @@
<template> <template>
<div class="render"> <div class="render">
<div class="content" id="render-content" v-html="content" @click="viewImg" style="padding:10px"></div> <div class="content markdown-body" id="render-content" v-html="content" @click="viewImg" :style="{'padding-top':paddingTop,'padding-left':paddingLeft}"></div>
<!-- <el-dialog class="white-bg-modal image-modal" :visible.sync="imageModalVisible" top="5vh" width="100%"> <!-- <el-dialog class="white-bg-modal image-modal" :visible.sync="imageModalVisible" top="5vh" width="100%">
<el-row align="middle" justify="center" @click.native="cancelViewImage"> <el-row align="middle" justify="center" @click.native="cancelViewImage">
<el-col :xs="{span:24,offset:0}" :sm="{span:24,offset:0}" :md="{span: 24,offset:0}" :lg="{ span: 24, offset: 0 }"> <el-col :xs="{span:24,offset:0}" :sm="{span:24,offset:0}" :md="{span: 24,offset:0}" :lg="{ span: 24, offset: 0 }">
@ -13,7 +13,7 @@
<script> <script>
export default { export default {
props: ['content'], props: ['content','paddingTop','paddingLeft'],
data() { data() {
return { return {
imageModalVisible: false, imageModalVisible: false,

@ -67,7 +67,7 @@
</el-popover> </el-popover>
<router-link <router-link
v-if="this.$store.state.user.token!=''" v-if="this.$store.state.user.token!=''"
to="/x/article/new" to="/dev/article/new"
class="margin-right-20" class="margin-right-20"
style="text-decoration:none;color:black;background:#66e2d5;padding:2px 12px;border:2px solid #0a0a0a;border-radius:3px;font-weight:bold;font-size:14px" style="text-decoration:none;color:black;background:#66e2d5;padding:2px 12px;border:2px solid #0a0a0a;border-radius:3px;font-weight:bold;font-size:14px"
>WRITE A POST</router-link> >WRITE A POST</router-link>

Loading…
Cancel
Save