diff --git a/internal/api_handler.go b/internal/api_handler.go index c0895913..af09bada 100644 --- a/internal/api_handler.go +++ b/internal/api_handler.go @@ -18,6 +18,7 @@ func apiHandler(e *echo.Echo) { e.GET("/web/article/detail", post.GetArticleDetail) e.GET("/web/article/beforeEdit", post.BeforeEditAr, user.CheckSignIn) e.POST("/web/article/saveChanges", post.SaveArticleChanges, user.CheckSignIn) + e.POST("/article/like", post.ArticleLike, user.CheckSignIn) // comment apis e.POST("/web/comment/create", post.Comment, user.CheckSignIn) diff --git a/internal/post/article.go b/internal/post/article.go index 0cc32d16..18c44a8c 100644 --- a/internal/post/article.go +++ b/internal/post/article.go @@ -3,8 +3,10 @@ package post import ( "encoding/json" "net/http" + "strconv" "time" + "github.com/gocql/gocql" "github.com/labstack/echo" "github.com/thinkindev/im.dev/internal/ecode" "github.com/thinkindev/im.dev/internal/misc" @@ -26,8 +28,8 @@ type ArContent struct { // NewArticle create a new article func NewArticle(c echo.Context) error { - opType := c.FormValue("type") - if opType != "1" && opType != "2" { + opType, _ := strconv.Atoi(c.FormValue("type")) + if opType != PostDraft && opType != PostPublished { return c.JSON(http.StatusBadRequest, misc.HTTPResp{ ErrCode: ecode.ParamInvalid, Message: ecode.ParamInvalidMsg, @@ -52,8 +54,16 @@ func NewArticle(c echo.Context) error { // modify render ar.Render = modify(ar.Render) - err = misc.CQL.Query(`INSERT INTO article (id,uid,title,tags,md,render,status,edit_date,lang) - VALUES (?,?,?,?,?,?,?,?,?)`, ar.ID, sess.ID, ar.Title, ar.Tags, ar.MD, ar.Render, opType, time.Now().Unix(), ar.Lang).Exec() + words := countWords(ar.MD) + var q *gocql.Query + if opType == PostDraft { + q = misc.CQL.Query(`INSERT INTO article (id,uid,title,tags,md,render,words,status,edit_date,lang) + VALUES (?,?,?,?,?,?,?,?,?,?)`, ar.ID, sess.ID, ar.Title, ar.Tags, ar.MD, ar.Render, words, PostDraft, time.Now().Unix(), ar.Lang) + } else { // publish + q = misc.CQL.Query(`INSERT INTO article (id,uid,title,tags,md,render,words,status,publish_date,edit_date,lang) + VALUES (?,?,?,?,?,?,?,?,?,?,?)`, ar.ID, sess.ID, ar.Title, ar.Tags, ar.MD, ar.Render, words, PostPublished, time.Now().Unix(), 0, ar.Lang) + } + err = q.Exec() if err != nil { misc.Log.Warn("access database error", zap.Error(err)) return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ @@ -79,6 +89,9 @@ type ArticleDetail struct { PublishDate string `json:"publish_date"` EditDate string `json:"edit_date"` Lang string `json:"lang"` + Words int `json:"words"` + Likes int `json:"likes"` // all likes of this article + Liked bool `json:"liked"` // current user liked this article pubDate int64 editDate int64 } @@ -94,8 +107,8 @@ func GetArticleDetail(c echo.Context) error { } detail := &ArticleDetail{ID: arID} - err := misc.CQL.Query(`SELECT uid,title,tags,render,status,publish_date,edit_date,lang FROM article WHERE id=?`, arID).Scan( - &detail.UID, &detail.Title, &detail.Tags, &detail.Render, &detail.Status, &detail.pubDate, &detail.editDate, &detail.Lang, + err := misc.CQL.Query(`SELECT uid,title,tags,render,words,status,publish_date,edit_date,lang FROM article WHERE id=?`, arID).Scan( + &detail.UID, &detail.Title, &detail.Tags, &detail.Render, &detail.Words, &detail.Status, &detail.pubDate, &detail.editDate, &detail.Lang, ) if err != nil { if err.Error() == misc.CQLNotFound { @@ -112,11 +125,26 @@ func GetArticleDetail(c echo.Context) error { } if detail.pubDate != 0 { - detail.PublishDate = utils.Time2ReadableString(time.Unix(detail.pubDate, 0)) + detail.PublishDate = utils.Time2EnglishString(time.Unix(detail.pubDate, 0)) } if detail.editDate != 0 { - detail.EditDate = utils.Time2ReadableString(time.Unix(detail.editDate, 0)) + detail.EditDate = utils.Time2EnglishString(time.Unix(detail.editDate, 0)) + } + + // if user signin, get his liked about this article + sess := user.GetSession(c) + if sess != nil { + var date int64 + q := misc.CQL.Query("SELECT input_date FROM post_like WHERE post_id=? and uid=?", arID, sess.ID) + q.Scan(&date) + if date != 0 { + detail.Liked = true + } } + + // get how many user like this article + misc.CQL.Query("SELECT likes FROM post_counter WHERE id=?", arID).Scan(&detail.Likes) + return c.JSON(http.StatusOK, misc.HTTPResp{ Data: detail, }) @@ -215,8 +243,9 @@ func SaveArticleChanges(c echo.Context) error { // modify render ar.Render = modify(ar.Render) - err = misc.CQL.Query(`UPDATE article SET title=?,tags=?,md=?,render=?,edit_date=?,lang=? WHERE id=?`, - ar.Title, ar.Tags, ar.MD, ar.Render, time.Now().Unix(), ar.Lang, ar.ID).Exec() + words := countWords(ar.MD) + err = misc.CQL.Query(`UPDATE article SET title=?,tags=?,md=?,render=?,words=?,edit_date=?,lang=? WHERE id=?`, + ar.Title, ar.Tags, ar.MD, ar.Render, words, time.Now().Unix(), ar.Lang, ar.ID).Exec() if err != nil { misc.Log.Warn("access database error", zap.Error(err)) return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ @@ -239,3 +268,85 @@ func saveTags(arID string, tags []string) { } } } + +// ArticleLike means a user like this article +func ArticleLike(c echo.Context) error { + id := c.FormValue("id") + tp := c.FormValue("type") + if id == "" || (tp != "1" && tp != "2") { + return c.JSON(http.StatusBadRequest, misc.HTTPResp{ + ErrCode: ecode.ParamInvalid, + Message: ecode.ParamInvalidMsg, + }) + } + + sess := user.GetSession(c) + + // check already liked + var pid string + q := misc.CQL.Query("SELECT post_id FROM post_like WHERE post_id=? and uid=?", id, sess.ID) + err := q.Scan(&pid) + if err != nil { + if err.Error() != misc.CQLNotFound { + misc.Log.Warn("access database error", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + } + + if tp == "1" { // like + if pid == id { + misc.Log.Info("someone is try to attack imdev server", zap.String("remote_ip", c.RealIP())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + q = misc.CQL.Query("INSERT INTO post_like (post_id,uid,type,input_date) VALUES (?,?,?,?)", + id, sess.ID, OpPostLike, time.Now().Unix()) + err = q.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, + }) + } + + q = misc.CQL.Query("UPDATE post_counter SET likes=likes + 1 WHERE id=?", id) + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err)) + } + } else { // cancel like + if pid != id { + misc.Log.Info("someone is try to attack imdev server", zap.String("remote_ip", c.RealIP())) + return c.JSON(http.StatusInternalServerError, misc.HTTPResp{ + ErrCode: ecode.DatabaseError, + Message: ecode.CommonErrorMsg, + }) + } + + q = misc.CQL.Query("DELETE FROM post_like WHERE post_id=? and uid=?", + id, sess.ID) + err = q.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, + }) + } + + q = misc.CQL.Query("UPDATE post_counter SET likes=likes - 1 WHERE id=?", id) + err = q.Exec() + if err != nil { + misc.Log.Warn("access database error", zap.Error(err)) + } + } + + return c.JSON(http.StatusOK, misc.HTTPResp{}) +} diff --git a/internal/post/const.go b/internal/post/const.go index f9988b0e..37623dbd 100644 --- a/internal/post/const.go +++ b/internal/post/const.go @@ -15,10 +15,15 @@ const ( ) const ( - // OpCommentLike means a user like a post + // OpCommentLike means a user like a comment OpCommentLike = 1 - // OpCommentDislike means a user dislike a post + // OpCommentDislike means a user dislike a comment OpCommentDislike = 2 + + // OpPostLike means a user like a post + OpPostLike = 1 + // OpPostDislike means a user dislike a post + OpPostDislike = 2 ) const ( diff --git a/internal/post/post.go b/internal/post/post.go index 235520f8..f852926f 100644 --- a/internal/post/post.go +++ b/internal/post/post.go @@ -1 +1,74 @@ package post + +import "strings" + +// 空格先行分割 +//1.数字和英文字母 遇到2类字符或者空格计数一 +//2.其余字符计数一 + +// 这段代码因为考虑各种情况,略复杂,后续我会整理下 +// @todo +func countWords(md string) int { + mds := strings.Split(md, " ") + + count := 0 + //上一个字符是否是特殊字符(非英文字母和数字) + special := false + lastCountSpecial := false + var last rune + var old rune + isfirst := false + for _, words := range mds { + last = rune(0) + special = false + lastCountSpecial = false + isfirst = true + for _, r := range words { + if special { + //如果上一个字符是特殊字符,那么当前字符无论是什么,都计数+1 + count++ + lastCountSpecial = true + } else { + //如果是首字符,+1 + if isfirst { + count++ + } else { + //如果上一个字符不是特殊字符,那么当前字符必须是特殊字符才能计数+1,否则认为是连续的 + if !isNumber(r) && !isAlpha(r) { + count++ + } + } + + if isNumber(r) || isAlpha(r) { + lastCountSpecial = false + } + } + //判断当前字符是否是特殊字符 + special = !isNumber(r) && !isAlpha(r) + old = last + last = r + isfirst = false + } + //如果当前非特殊字符是最后一个字符,那么计数+1 + if !special && lastCountSpecial && (isNumber(old) || isAlpha(old)) { + count++ + } + + } + + return count +} + +func isNumber(c rune) bool { + if c >= 48 && c <= 57 { + return true + } + return false +} + +func isAlpha(c rune) bool { + if (c >= 65 && c <= 90) || (c >= 97 && c <= 122) { + return true + } + return false +} diff --git a/internal/utils/time.go b/internal/utils/time.go index 7e0c731d..c2cc3752 100644 --- a/internal/utils/time.go +++ b/internal/utils/time.go @@ -6,14 +6,6 @@ import ( "time" ) -// Time2ReadableString converts time to readable string -// 1分钟之内,显示xx秒前 -// 1小时之内,显示XX分钟前 -// 24小时之内,显示xx小时前 -// 昨天 x:x -// 前天 x:x -// 同一年,显示x月xx -// 不同年,显示xx.xx.xx func Time2ReadableString(t time.Time) string { now := time.Now().Local() intv := now.Unix() - t.Unix() @@ -36,9 +28,35 @@ func Time2ReadableString(t time.Time) string { return fmt.Sprintf("yestoday %02d:%02d", h, m) } - if y1 == y2 { - return fmt.Sprintf("%02d.%02d", m2, d2) + return Time2EnglishString(t) +} + +func Time2String(t time.Time) string { + return t.Format("2006-01-02 15:04:05.999") +} + +var months = map[int]string{ + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", +} + +func Time2EnglishString(t time.Time) string { + now := time.Now() + // 检查是否是同一年 + y, m, d := t.Date() + if now.Year() == y { + return fmt.Sprintf("%s %d", months[int(m)], d) } - return t.Format("06.1.2") + return fmt.Sprintf("%s %d,%d", months[int(m)], d, y) } diff --git a/quick-start/cql/start.cql b/quick-start/cql/start.cql index 043a3593..8ed9b6d8 100644 --- a/quick-start/cql/start.cql +++ b/quick-start/cql/start.cql @@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS article ( md text, -- markdown render text, -- rendered html status tinyint, -- 1. draft 2. published 3. delete + words int, -- word count publish_date bigint, edit_date bigint, @@ -80,17 +81,6 @@ 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 - - comments counter, - likes counter, - recommands counter, - - PRIMARY KEY (id) -) WITH gc_grace_seconds = 10800; - CREATE TABLE IF NOT EXISTS comment_counter ( id text, -- comment id likes counter, @@ -107,3 +97,25 @@ CREATE TABLE IF NOT EXISTS comment_like ( ) WITH gc_grace_seconds = 10800; CREATE CUSTOM INDEX IF NOT EXISTS ON comment_like (uid) USING 'org.apache.cassandra.index.sasi.SASIIndex' ; + + +CREATE TABLE IF NOT EXISTS post_counter ( + id text, -- post id + + comments counter, + likes counter, + recommands counter, + + PRIMARY KEY (id) +) WITH gc_grace_seconds = 10800; + +-- record whether a user likes a post +CREATE TABLE IF NOT EXISTS post_like ( + post_id text, -- post id + uid text, -- user id + type tinyint, -- 1: like 2: dislike + input_date bigint, + PRIMARY KEY (post_id,uid) +) WITH gc_grace_seconds = 10800; +CREATE CUSTOM INDEX IF NOT EXISTS ON post_like (uid) + USING 'org.apache.cassandra.index.sasi.SASIIndex' ; diff --git a/ui/src/assets/icons/downvote.svg b/ui/src/assets/icons/downvote.svg deleted file mode 100644 index fdc2c69c..00000000 --- a/ui/src/assets/icons/downvote.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/assets/icons/upvote.svg b/ui/src/assets/icons/upvote.svg deleted file mode 100644 index c37c03e9..00000000 --- a/ui/src/assets/icons/upvote.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/assets/svg_icons/clap.svg b/ui/src/assets/svg_icons/clap.svg new file mode 100644 index 00000000..f97851b8 --- /dev/null +++ b/ui/src/assets/svg_icons/clap.svg @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/ui/src/theme/eleui-style.less b/ui/src/theme/eleui-style.less index 0f2df11d..9eae82b0 100644 --- a/ui/src/theme/eleui-style.less +++ b/ui/src/theme/eleui-style.less @@ -1,6 +1,8 @@ // overide global focus style h1 { font-size: 36px; + margin-block-start: .4em; + margin-block-end: .4em; } h3 { diff --git a/ui/src/theme/light/style.less b/ui/src/theme/light/style.less index bb477bf4..5c6d5d1a 100644 --- a/ui/src/theme/light/style.less +++ b/ui/src/theme/light/style.less @@ -88,15 +88,15 @@ color: rgba(0,0,0,.8) } } - } + } .clap { position: relative; outline: 1px solid transparent; border-radius: 50%; border: 1px solid #bdc3c7; - width: 60px; - height: 60px; + width: 40px; + height: 40px; background: none; } .clap:after { @@ -106,8 +106,8 @@ left: 0; display: block; border-radius: 50%; - width: 59px; - height: 59px; + width: 39px; + height: 39px; } .clap:hover { cursor: pointer; @@ -118,7 +118,7 @@ animation: shockwave 1s ease-in infinite; } .clap svg { - width: 30px; + width: 20px; fill: none; stroke: #333; stroke-width: 1px; @@ -236,18 +236,7 @@ border-top: 1px solid #ddd } - .content h1, - .content h2, - .content h3, - .content h4, - .content h5, - .content h6 { - margin: 0 0 12px; - font-weight: 700; - color: #2f2f2f; - line-height: 1.6; - text-rendering: optimizelegibility - } + .content h1 { font-size: 26px diff --git a/ui/src/theme/light/var.less b/ui/src/theme/light/var.less index c2473fdc..ba0ac1b9 100644 --- a/ui/src/theme/light/var.less +++ b/ui/src/theme/light/var.less @@ -1,4 +1,4 @@ -@grey-color: rgb(124,124,124); +@grey-color: rgba(0,0,0,.54); @light-grey-color: rgb(135, 138, 140); @main-color: #303133; diff --git a/ui/src/theme/style.less b/ui/src/theme/style.less index e185a63e..d7985272 100644 --- a/ui/src/theme/style.less +++ b/ui/src/theme/style.less @@ -7,14 +7,14 @@ .margin-right-5 {margin-right: 5px};.margin-right-10 {margin-right: 10px};.margin-right-15 {margin-right: 15px};.margin-right-20 {margin-right: 20px};.margin-right-30 {margin-right: 30px};.margin-right-40 {margin-right: 40px}; .margin-top-2 {margin-top: 2px};.margin-top-5 {margin-top: 5px};.margin-top-8 {margin-top: 8px};.margin-top-10 {margin-top: 10px};.margin-top-15 {margin-top: 15px};.margin-top-20 {margin-top: 20px};.margin-top-25 {margin-top: 25px};.margin-top-30 {margin-top: 30px};.margin-top-40 {margin-top: 40px};.margin-top-60 {margin-top: 60px};.margin-top-80 {margin-top: 80px};.margin-top-100 {margin-top: 100px}; -.margin-top--35 {margin-top: -35px};.margin-top--40 {margin-top: -40px};.margin-top--50 {margin-top: -50px}; +.margin-top--30 {margin-top: -30px};.margin-top--35 {margin-top: -35px};.margin-top--40 {margin-top: -40px};.margin-top--50 {margin-top: -50px}; .margin-bottom-5 {margin-bottom: 5px};.margin-bottom-10 {margin-bottom: 10px};.margin-bottom-15 {margin-bottom: 15px};.margin-bottom-20 { margin-bottom: 20px}.margin-bottom-30 {margin-bottom: 30px };.margin-bottom-40 {margin-bottom: 40px };.margin-bottom-50 {margin-bottom: 50px }; /* ----------------------------padding------------------------ */ .padding-5 {padding: 5px 5px};.padding-10 { padding: 10px 10px};.padding-20 {padding: 20px 20px}; -.padding-left-5 {padding-left: 5px};.padding-left-10 {padding-left: 10px};.padding-left-15 {padding-left: 15px};.padding-left-20 {padding-left: 20px}; +.padding-left-5 {padding-left: 5px};.padding-left-8 {padding-left: 8px};.padding-left-10 {padding-left: 10px};.padding-left-15 {padding-left: 15px};.padding-left-20 {padding-left: 20px}; .padding-right-5 {padding-right: 5px}.padding-right-10 {padding-right: 10px};.padding-right-15 {padding-right: 15px};.padding-right-20 {padding-right: 20px}; @@ -23,9 +23,9 @@ .padding-bottom-5 { padding-bottom: 5px};.padding-bottom-10 {padding-bottom: 10px};.padding-bottom-15 {padding-bottom: 15px};.padding-bottom-20 {padding-bottom: 20px};.padding-bottom-30 {padding-bottom: 30px};.padding-bottom-40 {padding-bottom: 40px}; /* ----------------------------height/width------------------------ */ -.height-45{height:45px};.height-50{height:50px};.height-100{height:100px};.height-150{height:150px};.height-200{height:200px}; +.height-40{height:40px};.height-45{height:45px};.height-50{height:50px};.height-100{height:100px};.height-150{height:150px};.height-200{height:200px}; -.width-50{width:50px};.width-100{width:100px};.width-150{width:150px};.width-200{width:200px};.width-300{width:300px}; +.width-40{width:40px};.width-50{width:50px};.width-100{width:100px};.width-150{width:150px};.width-200{width:200px};.width-300{width:300px}; .width-100p {width: 100%}; diff --git a/ui/src/views/article/detail.vue b/ui/src/views/article/detail.vue index 63eae382..02947b7e 100644 --- a/ui/src/views/article/detail.vue +++ b/ui/src/views/article/detail.vue @@ -9,7 +9,7 @@ >