parent
99ca85c0dc
commit
1e87845534
@ -0,0 +1,264 @@
|
|||||||
|
package biz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
pb "moredoc/api/v1"
|
||||||
|
"moredoc/middleware/auth"
|
||||||
|
"moredoc/model"
|
||||||
|
"moredoc/util"
|
||||||
|
"moredoc/util/captcha"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentAPIService struct {
|
||||||
|
pb.UnimplementedCommentAPIServer
|
||||||
|
dbModel *model.DBModel
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommentAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *CommentAPIService) {
|
||||||
|
return &CommentAPIService{dbModel: dbModel, logger: logger.Named("CommentAPIService")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommentAPIService) checkLogin(ctx context.Context) (*auth.UserClaims, error) {
|
||||||
|
return checkGRPCLogin(s.dbModel, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommentAPIService) checkPermission(ctx context.Context) (*auth.UserClaims, error) {
|
||||||
|
return checkGRPCPermission(s.dbModel, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发表评论
|
||||||
|
func (s *CommentAPIService) CreateComment(ctx context.Context, req *pb.CreateCommentRequest) (*emptypb.Empty, error) {
|
||||||
|
userClaims, err := checkGRPCLogin(s.dbModel, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论验证码错误
|
||||||
|
cfg := s.dbModel.GetConfigOfSecurity(model.ConfigSecurityEnableCaptchaComment)
|
||||||
|
if cfg.EnableCaptchaComment && !captcha.VerifyCaptcha(req.CaptchaId, req.Captcha) {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "验证码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := &model.Comment{}
|
||||||
|
err = util.CopyStruct(req, comment)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("CreateDocument", zap.Error(err))
|
||||||
|
return nil, status.Errorf(codes.Internal, "发布评论失败:"+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.DocumentId <= 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "文档id不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.Content == "" {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "评论内容不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultStatus, err := s.dbModel.CanIPublishComment(userClaims.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
comment.Status = defaultStatus
|
||||||
|
comment.UserId = userClaims.UserId
|
||||||
|
err = s.dbModel.CreateComment(comment)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("CreateComment", zap.Error(err))
|
||||||
|
return nil, status.Errorf(codes.Internal, "发布评论失败:"+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新评论,仅限管理员
|
||||||
|
func (s *CommentAPIService) UpdateComment(ctx context.Context, req *pb.Comment) (*emptypb.Empty, error) {
|
||||||
|
userClaims, err := s.checkPermission(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Debug("UpdateComment", zap.Any("user", userClaims), zap.Any("req", req))
|
||||||
|
|
||||||
|
// 只允许更新评论状态和内容
|
||||||
|
updateFields := []string{"content", "status"}
|
||||||
|
comment := &model.Comment{}
|
||||||
|
util.CopyStruct(req, comment)
|
||||||
|
err = s.dbModel.UpdateComment(comment, updateFields...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "更新评论失败")
|
||||||
|
}
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录的用户,可删除自己的评论
|
||||||
|
// 管理员,可删除任意评论
|
||||||
|
func (s *CommentAPIService) DeleteComment(ctx context.Context, req *pb.DeleteCommentRequest) (*emptypb.Empty, error) {
|
||||||
|
var userIds []int64
|
||||||
|
userClaims, err := s.checkPermission(ctx)
|
||||||
|
if err != nil && userClaims == nil {
|
||||||
|
// 未登录用户
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 已登录用户,判断有管理员权限
|
||||||
|
isAdmin := userClaims.UserId > 0 && err == nil
|
||||||
|
if !isAdmin {
|
||||||
|
// 非管理员,限定只能删除自己的评论
|
||||||
|
userIds = append(userIds, userClaims.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.dbModel.DeleteComment(req.Id, userIds...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "删除评论失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommentAPIService) GetComment(ctx context.Context, req *pb.GetCommentRequest) (*pb.Comment, error) {
|
||||||
|
_, err := s.checkPermission(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.logger.Debug("GetComment", zap.Any("req", req))
|
||||||
|
comment, err := s.dbModel.GetComment(req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "获取评论失败:"+err.Error())
|
||||||
|
}
|
||||||
|
user, _ := s.dbModel.GetUser(comment.UserId, model.UserPublicFields...)
|
||||||
|
|
||||||
|
pbComment := &pb.Comment{}
|
||||||
|
util.CopyStruct(comment, pbComment)
|
||||||
|
util.CopyStruct(user, pbComment.User)
|
||||||
|
|
||||||
|
return pbComment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommentAPIService) ListComment(ctx context.Context, req *pb.ListCommentRequest) (*pb.ListCommentReply, error) {
|
||||||
|
var (
|
||||||
|
isLogin bool
|
||||||
|
isAdmin bool
|
||||||
|
)
|
||||||
|
|
||||||
|
userClaims, err := s.checkPermission(ctx)
|
||||||
|
if err != nil && userClaims == nil {
|
||||||
|
// 未登录用户
|
||||||
|
isLogin = false
|
||||||
|
} else {
|
||||||
|
// 已登录用户,判断有管理员权限
|
||||||
|
isLogin = true
|
||||||
|
isAdmin = userClaims.UserId > 0 && err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := &model.OptionGetCommentList{
|
||||||
|
Page: int(req.Page),
|
||||||
|
Size: int(req.Size_),
|
||||||
|
WithCount: true,
|
||||||
|
QueryIn: make(map[string][]interface{}),
|
||||||
|
QueryLike: make(map[string][]interface{}),
|
||||||
|
Sort: []string{req.Order},
|
||||||
|
}
|
||||||
|
|
||||||
|
// default status
|
||||||
|
opt.QueryIn["status"] = []interface{}{model.CommentStatusApproved}
|
||||||
|
|
||||||
|
if req.DocumentId > 0 {
|
||||||
|
opt.QueryIn["document_id"] = []interface{}{req.DocumentId}
|
||||||
|
opt.Page = 1
|
||||||
|
opt.Size = 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ParentId) > 0 {
|
||||||
|
opt.QueryIn["parent_id"] = util.Slice2Interface(req.ParentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin && req.Wd != "" {
|
||||||
|
opt.QueryLike["content"] = []interface{}{req.Wd}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogin && req.UserId == userClaims.UserId) || isAdmin {
|
||||||
|
delete(opt.QueryIn, "status")
|
||||||
|
if len(req.Status) > 0 {
|
||||||
|
opt.QueryIn["status"] = util.Slice2Interface(req.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.UserId > 0 {
|
||||||
|
opt.QueryIn["user_id"] = []interface{}{req.UserId}
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, total, err := s.dbModel.GetCommentList(opt)
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, status.Errorf(codes.Internal, "获取评论列表失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.ListCommentReply{
|
||||||
|
Total: total,
|
||||||
|
}
|
||||||
|
util.CopyStruct(comments, &resp.Comment)
|
||||||
|
|
||||||
|
var (
|
||||||
|
userIds []int64
|
||||||
|
documentIds []int64
|
||||||
|
userIdMapCommentIdx = make(map[int64][]int)
|
||||||
|
documentIdMapCommentIdx = make(map[int64][]int)
|
||||||
|
)
|
||||||
|
for idx, comment := range comments {
|
||||||
|
userIds = append(userIds, comment.UserId)
|
||||||
|
documentIds = append(documentIds, comment.DocumentId)
|
||||||
|
userIdMapCommentIdx[comment.UserId] = append(userIdMapCommentIdx[comment.UserId], idx)
|
||||||
|
documentIdMapCommentIdx[comment.DocumentId] = append(documentIdMapCommentIdx[comment.DocumentId], idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userIds) > 0 {
|
||||||
|
users, _, _ := s.dbModel.GetUserList(&model.OptionGetUserList{
|
||||||
|
SelectFields: model.UserPublicFields,
|
||||||
|
WithCount: false,
|
||||||
|
QueryIn: map[string][]interface{}{"id": util.Slice2Interface(userIds)},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
indexes := userIdMapCommentIdx[user.Id]
|
||||||
|
for _, idx := range indexes {
|
||||||
|
util.CopyStruct(user, &resp.Comment[idx].User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(documentIds) > 0 {
|
||||||
|
documents, _, _ := s.dbModel.GetDocumentList(&model.OptionGetDocumentList{
|
||||||
|
SelectFields: []string{"id", "title"},
|
||||||
|
WithCount: false,
|
||||||
|
QueryIn: map[string][]interface{}{"id": util.Slice2Interface(documentIds)},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, document := range documents {
|
||||||
|
indexes := documentIdMapCommentIdx[document.Id]
|
||||||
|
for _, idx := range indexes {
|
||||||
|
resp.Comment[idx].DocumentTitle = document.Title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommentAPIService) CheckComment(ctx context.Context, req *pb.CheckCommentRequest) (*emptypb.Empty, error) {
|
||||||
|
_, err := s.checkPermission(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.dbModel.UpdateCommentStatus(req.Id, req.Status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, err.Error())
|
||||||
|
}
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import service from '~/utils/request'
|
||||||
|
|
||||||
|
export const createComment = (data) => {
|
||||||
|
return service({
|
||||||
|
url: '/api/v1/comment',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateComment = (data) => {
|
||||||
|
return service({
|
||||||
|
url: '/api/v1/comment',
|
||||||
|
method: 'put',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteComment = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/api/v1/comment',
|
||||||
|
method: 'delete',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComment = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/api/v1/comment',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listComment = (params) => {
|
||||||
|
return service({
|
||||||
|
url: '/api/v1/comment/list',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkComment = (data) => {
|
||||||
|
return service({
|
||||||
|
url: '/api/v1/comment/check',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<el-row :class="`com-comment-item com-comment-item-${size}`">
|
||||||
|
<el-col :span="2">
|
||||||
|
<nuxt-link :to="{ name: 'user-id', params: { id: user.id } }"
|
||||||
|
><user-avatar :size="size == 'small' ? 40 : 48" :user="comment.user"
|
||||||
|
/></nuxt-link>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="22">
|
||||||
|
<div class="username">
|
||||||
|
<nuxt-link
|
||||||
|
class="el-link el-link--default"
|
||||||
|
:to="{ name: 'user-id', params: { id: comment.user_id } }"
|
||||||
|
>{{ comment.user.username }}</nuxt-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="comment-content">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span v-html="comment.reply_user" />
|
||||||
|
{{ comment.content }}
|
||||||
|
</div>
|
||||||
|
<div class="comment-action">
|
||||||
|
<el-row class="help-block">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-tooltip
|
||||||
|
:content="formatDatetime(comment.created_at)"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="el-icon-time"></i>
|
||||||
|
{{ formatRelativeTime(comment.created_at) }}
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" class="text-right">
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-chat-dot-square"
|
||||||
|
@click="reply"
|
||||||
|
>回复</el-button
|
||||||
|
>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<form-comment
|
||||||
|
v-if="replyComment"
|
||||||
|
:article-id="comment.article_id"
|
||||||
|
:parent-id="comment.id"
|
||||||
|
:placeholder="`回复 ${comment.user.username}`"
|
||||||
|
@success="commentSuccess"
|
||||||
|
/>
|
||||||
|
<slot></slot>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import UserAvatar from '~/components/UserAvatar.vue'
|
||||||
|
import { formatRelativeTime, formatDatetime } from '~/utils/utils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommentItem',
|
||||||
|
components: { UserAvatar },
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'default', // default、small
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
id: 0,
|
||||||
|
parent_id: 0,
|
||||||
|
user_id: 0,
|
||||||
|
username: '匿名',
|
||||||
|
avatar: '',
|
||||||
|
group_id: 0,
|
||||||
|
verify_status: 0,
|
||||||
|
content: '内容加载中...',
|
||||||
|
created_at: '0000-00-00',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
replyComment: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('user', ['user']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatRelativeTime,
|
||||||
|
formatDatetime,
|
||||||
|
reply() {
|
||||||
|
this.replyComment = !this.replyComment
|
||||||
|
},
|
||||||
|
commentSuccess() {
|
||||||
|
this.$emit('success')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.com-comment-item {
|
||||||
|
font-size: 14px;
|
||||||
|
.comment-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #f5f7f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #565656;
|
||||||
|
span {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.username a {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.com-comment-item-small {
|
||||||
|
font-size: 13px;
|
||||||
|
.el-col-2 {
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
.el-col-22 {
|
||||||
|
width: 93%;
|
||||||
|
}
|
||||||
|
.comment-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div class="com-comment-list">
|
||||||
|
<comment-item
|
||||||
|
v-for="comment in comments"
|
||||||
|
:key="'comment-' + comment.id"
|
||||||
|
:comment="comment"
|
||||||
|
@success="commentSuccess"
|
||||||
|
>
|
||||||
|
<comment-item
|
||||||
|
v-for="child in comment.children"
|
||||||
|
:key="'comment-' + child.id"
|
||||||
|
:comment="child"
|
||||||
|
:size="'small'"
|
||||||
|
@success="commentSuccess"
|
||||||
|
></comment-item>
|
||||||
|
</comment-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import CommentItem from '~/components/CommentItem.vue'
|
||||||
|
import { listComment } from '~/api/comment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommentList',
|
||||||
|
components: { CommentItem },
|
||||||
|
props: {
|
||||||
|
articleId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
parentId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
comments: [],
|
||||||
|
req: {
|
||||||
|
article_id: this.articleId,
|
||||||
|
parent_id: this.parentId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('user', ['user']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
articleId: {
|
||||||
|
handler(val) {
|
||||||
|
this.req.article_id = val
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
parentId: {
|
||||||
|
handler(val) {
|
||||||
|
this.req.parent_id = val
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getComments()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 获取文章评论列表
|
||||||
|
async getComments() {
|
||||||
|
if (!this.req.article_id) return
|
||||||
|
const res = await listComment({
|
||||||
|
article_id: this.articleId,
|
||||||
|
order: 'id asc',
|
||||||
|
})
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.comments = this.comments2tree(res.data.comment || [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
commentSuccess() {
|
||||||
|
this.getComments()
|
||||||
|
},
|
||||||
|
comments2tree(comments) {
|
||||||
|
const tree = []
|
||||||
|
const map = {}
|
||||||
|
comments.forEach((comment) => {
|
||||||
|
map[comment.id] = comment
|
||||||
|
})
|
||||||
|
comments.forEach((comment) => {
|
||||||
|
// 寻找最顶层父级
|
||||||
|
let parent = map[comment.parent_id]
|
||||||
|
let replyUser = ''
|
||||||
|
if (comment.parent_id && parent) {
|
||||||
|
try {
|
||||||
|
replyUser = `<a href="/user/${parent.user.id}" class="el-link el-link--primary" target="blank">@${parent.user.username}</a>`
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
while (parent && parent.parent_id) {
|
||||||
|
parent = map[parent.parent_id]
|
||||||
|
}
|
||||||
|
comment.reply_user = replyUser
|
||||||
|
if (parent) {
|
||||||
|
;(parent.children || (parent.children = [])).push(comment)
|
||||||
|
} else {
|
||||||
|
tree.push(comment)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tree
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.com-comment-list {
|
||||||
|
& > .el-row {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-bottom: 1px solid #efefef;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
& > .el-row:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="com-form-comment-check">
|
||||||
|
<el-form
|
||||||
|
ref="form"
|
||||||
|
:model="icomment"
|
||||||
|
class="form-comment-check"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item prop="content" label="评论内容">
|
||||||
|
<el-input
|
||||||
|
v-model="icomment.content"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="审核状态">
|
||||||
|
<el-radio-group v-model="icomment.status">
|
||||||
|
<el-radio :label="0">待审核</el-radio>
|
||||||
|
<el-radio :label="1">审核通过</el-radio>
|
||||||
|
<el-radio :label="2">审核拒绝</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="btn-block"
|
||||||
|
icon="el-icon-check"
|
||||||
|
@click="onSubmit"
|
||||||
|
>提交</el-button
|
||||||
|
>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { updateComment } from '~/api/comment'
|
||||||
|
export default {
|
||||||
|
name: 'FormCommentCheck',
|
||||||
|
props: {
|
||||||
|
comment: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
icomment: {
|
||||||
|
id: 0,
|
||||||
|
content: '',
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
comment: {
|
||||||
|
handler(val) {
|
||||||
|
this.icomment = { status: 0, ...val }
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onSubmit() {
|
||||||
|
const res = await updateComment(this.icomment)
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.$message.success('更新成功')
|
||||||
|
this.$emit('success')
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="com-user-avatar">
|
||||||
|
<el-avatar
|
||||||
|
:size="size"
|
||||||
|
:alt="user.username"
|
||||||
|
:src="user.avatar"
|
||||||
|
@error="errorAvatar"
|
||||||
|
>
|
||||||
|
<img src="/static/images/avatar.png" />
|
||||||
|
</el-avatar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UserAvatar',
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
errorAvatar() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.com-user-avatar {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
.el-avatar {
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
padding: 3px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #fff;
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid #409eff;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,16 +1,271 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div>{{ $route.name }}</div>
|
<el-card shadow="never" class="search-card">
|
||||||
|
<FormSearch
|
||||||
|
:fields="searchFormFields"
|
||||||
|
:loading="loading"
|
||||||
|
:show-create="false"
|
||||||
|
:show-delete="true"
|
||||||
|
:disabled-delete="selectedRow.length === 0"
|
||||||
|
@onSearch="onSearch"
|
||||||
|
@onCreate="onCreate"
|
||||||
|
@onDelete="batchDelete"
|
||||||
|
>
|
||||||
|
<template slot="buttons">
|
||||||
|
<el-dropdown
|
||||||
|
:disabled="selectedRow.length === 0"
|
||||||
|
@command="checkComment"
|
||||||
|
>
|
||||||
|
<el-button type="primary">
|
||||||
|
批量审批 <i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item :command="1">审核通过</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="2">审核拒绝</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="0">变为待审</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</FormSearch>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" class="mgt-20px">
|
||||||
|
<TableList
|
||||||
|
:loading="loading"
|
||||||
|
:table-data="comments"
|
||||||
|
:fields="tableListFields"
|
||||||
|
:show-actions="true"
|
||||||
|
:show-view="false"
|
||||||
|
:show-edit="true"
|
||||||
|
:show-delete="true"
|
||||||
|
:show-select="true"
|
||||||
|
:actions-min-width="160"
|
||||||
|
@selectRow="selectRow"
|
||||||
|
@editRow="editRow"
|
||||||
|
@deleteRow="deleteRow"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" class="mgt-20px">
|
||||||
|
<div class="text-right">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
:current-page="search.page"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:page-size="search.size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-dialog
|
||||||
|
v-if="comment.id > 0"
|
||||||
|
title="评论编审"
|
||||||
|
:visible.sync="formCommentVisible"
|
||||||
|
width="640px"
|
||||||
|
>
|
||||||
|
<FormCommentCheck
|
||||||
|
ref="formComment"
|
||||||
|
:comment="comment"
|
||||||
|
@success="formCommentSuccess"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
listComment,
|
||||||
|
deleteComment,
|
||||||
|
getComment,
|
||||||
|
checkComment,
|
||||||
|
} from '~/api/comment'
|
||||||
|
import TableList from '~/components/TableList.vue'
|
||||||
|
import FormSearch from '~/components/FormSearch.vue'
|
||||||
export default {
|
export default {
|
||||||
|
components: { TableList, FormSearch },
|
||||||
layout: 'admin',
|
layout: 'admin',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
formCommentVisible: false,
|
||||||
|
search: {
|
||||||
|
wd: '',
|
||||||
|
page: 1,
|
||||||
|
status: [],
|
||||||
|
size: 10,
|
||||||
|
order: 'id desc',
|
||||||
|
},
|
||||||
|
comments: [],
|
||||||
|
total: 0,
|
||||||
|
searchFormFields: [],
|
||||||
|
tableListFields: [],
|
||||||
|
selectedRow: [],
|
||||||
|
comment: { id: 0 },
|
||||||
|
}
|
||||||
|
},
|
||||||
head() {
|
head() {
|
||||||
return {
|
return {
|
||||||
title: `面板 - MOREDOC · 魔刀文库`,
|
title: `评论管理 - ${this.settings.system.sitename}`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$store.state.setting.settings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.initSearchForm()
|
||||||
|
this.initTableListFields()
|
||||||
|
await this.listComment()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async listComment() {
|
||||||
|
this.loading = true
|
||||||
|
const res = await listComment(this.search)
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.comments = (res.data.comment || []).map((item) => {
|
||||||
|
item.username = item.user.username
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
this.total = res.data.total
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.message)
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.search.size = val
|
||||||
|
this.listComment()
|
||||||
|
},
|
||||||
|
handlePageChange(val) {
|
||||||
|
this.search.page = val
|
||||||
|
this.listComment()
|
||||||
|
},
|
||||||
|
onSearch(search) {
|
||||||
|
this.search = { ...this.search, page: 1, ...search }
|
||||||
|
this.listComment()
|
||||||
|
},
|
||||||
|
onCreate() {
|
||||||
|
this.comment = { id: 0 }
|
||||||
|
this.formCommentVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.commentForm.reset()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async editRow(row) {
|
||||||
|
const res = await getComment({ id: row.id })
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.comment = res.data
|
||||||
|
this.formCommentVisible = true
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formCommentSuccess() {
|
||||||
|
this.formCommentVisible = false
|
||||||
|
this.listComment()
|
||||||
|
},
|
||||||
|
async checkComment(cmd) {
|
||||||
|
const res = await checkComment({
|
||||||
|
id: this.selectedRow.map((item) => item.id),
|
||||||
|
status: cmd,
|
||||||
|
})
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.$message.success('审批成功')
|
||||||
|
this.listComment()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$message.error(res.data.message || '审批失败')
|
||||||
|
},
|
||||||
|
batchDelete() {
|
||||||
|
this.$confirm(
|
||||||
|
`您确定要删除选中的【${this.selectedRow.length}条】评论吗?删除之后不可恢复!`,
|
||||||
|
'温馨提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
const ids = this.selectedRow.map((item) => item.id)
|
||||||
|
const res = await deleteComment({ id: ids })
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.$message.success('删除成功')
|
||||||
|
this.listComment()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
},
|
||||||
|
deleteRow(row) {
|
||||||
|
this.$confirm(`您确定要删除该评论吗?删除之后不可恢复!`, '温馨提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const res = await deleteComment({ id: row.id })
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.$message.success('删除成功')
|
||||||
|
this.listComment()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
},
|
||||||
|
selectRow(rows) {
|
||||||
|
this.selectedRow = rows
|
||||||
|
},
|
||||||
|
initSearchForm() {
|
||||||
|
this.searchFormFields = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
label: '关键字',
|
||||||
|
name: 'wd',
|
||||||
|
placeholder: '请输入关键字',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
label: '状态',
|
||||||
|
name: 'status',
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
multiple: true,
|
||||||
|
options: [
|
||||||
|
{ label: '审核拒绝', value: 2 },
|
||||||
|
{ label: '审核通过', value: 1 },
|
||||||
|
{ label: '待审核', value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
initTableListFields() {
|
||||||
|
this.tableListFields = [
|
||||||
|
{ prop: 'id', label: 'ID', width: 80, type: 'number', fixed: 'left' },
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 80,
|
||||||
|
type: 'enum',
|
||||||
|
fixed: 'left',
|
||||||
|
enum: {
|
||||||
|
2: { label: '审核拒绝', value: 2, type: 'danger' },
|
||||||
|
1: { label: '审核通过', value: 1, type: 'success' },
|
||||||
|
0: { label: '待审核', value: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ prop: 'document_title', label: '文档', minWidth: 150 },
|
||||||
|
{ prop: 'content', label: '评论内容', minWidth: 150 },
|
||||||
|
{ prop: 'username', label: '评论人', minWidth: 150 },
|
||||||
|
{ prop: 'created_at', label: '创建时间', width: 160, type: 'datetime' },
|
||||||
|
{ prop: 'updated_at', label: '更新时间', width: 160, type: 'datetime' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style></style>
|
||||||
|
Loading…
Reference in new issue