🎨 updated comment code framework / add comment verification
This commit is contained in:
18
components.d.ts
vendored
18
components.d.ts
vendored
@@ -27,7 +27,6 @@ declare module 'vue' {
|
|||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||||
AQrcode: typeof import('ant-design-vue/es')['QRCode']
|
AQrcode: typeof import('ant-design-vue/es')['QRCode']
|
||||||
ASegmented: typeof import('ant-design-vue/es')['Segmented']
|
|
||||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
@@ -36,15 +35,17 @@ declare module 'vue' {
|
|||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
|
BoxDog: typeof import('./src/Components/BoxDog/BoxDog.vue')['default']
|
||||||
Card3D: typeof import('./src/components/Card3D/Card3D.vue')['default']
|
Card3D: typeof import('./src/Components/Card3D/Card3D.vue')['default']
|
||||||
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
||||||
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
|
Clouds: typeof import('./src/Components/Clouds/Clouds.vue')['default']
|
||||||
CommentReply: typeof import('./src/components/CommentReply/CommentReply.vue')['default']
|
CommentInput: typeof import('./src/Components/CommentReply/Components/CommentInput/CommentInput.vue')['default']
|
||||||
|
CommentList: typeof import('./src/Components/CommentReply/Components/CommentList/CommentList.vue')['default']
|
||||||
|
CommentReply: typeof import('./src/Components/CommentReply/index.vue')['default']
|
||||||
CopyOutlined: typeof import('@ant-design/icons-vue')['CopyOutlined']
|
CopyOutlined: typeof import('@ant-design/icons-vue')['CopyOutlined']
|
||||||
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
|
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
|
||||||
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
DynamicTitle: typeof import('./src/Components/DynamicTitle/DynamicTitle.vue')['default']
|
||||||
EffectsCard: typeof import('./src/components/EffectsCard/EffectsCard.vue')['default']
|
EffectsCard: typeof import('./src/Components/EffectsCard/EffectsCard.vue')['default']
|
||||||
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
||||||
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
||||||
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
||||||
@@ -57,6 +58,9 @@ declare module 'vue' {
|
|||||||
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
||||||
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
|
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
|
||||||
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
|
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
|
||||||
|
ReplyInput: typeof import('./src/Components/CommentReply/Components/ReplyInput/ReplyInput.vue')['default']
|
||||||
|
ReplyList: typeof import('./src/Components/CommentReply/Components/ReplyList/ReplyList.vue')['default']
|
||||||
|
ReplyReply: typeof import('./src/Components/CommentReply/Components/ReplyReplyInput/ReplyReply.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined']
|
SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined']
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import {service} from "@/utils/alova/service.ts";
|
import {service} from "@/utils/alova/service.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取验证码图片数据
|
* 获取旋转验证码图片数据
|
||||||
*/
|
*/
|
||||||
export const getRotatedCaptchaData = () => {
|
export const getRotatedCaptchaData = () => {
|
||||||
return service.Get('/api/captcha/rotate/get', {
|
return service.Get('/api/captcha/rotate/get', {
|
||||||
@@ -29,3 +29,14 @@ export const checkRotatedCaptcha = (angle: any, key: any) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 获取滑动验证码图片数据
|
||||||
|
*/
|
||||||
|
export const getSlideCaptchaDataApi = () => {
|
||||||
|
return service.Get('/api/captcha/slide/generate', {
|
||||||
|
meta: {
|
||||||
|
ignoreToken: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
@@ -11,6 +11,8 @@ export const commentSubmitApi = (params: any) => {
|
|||||||
images: params.images,
|
images: params.images,
|
||||||
topic_id: params.topic_id,
|
topic_id: params.topic_id,
|
||||||
author: params.author,
|
author: params.author,
|
||||||
|
point: params.point,
|
||||||
|
key: params.key,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'comment-submit',
|
name: 'comment-submit',
|
||||||
@@ -33,6 +35,8 @@ export const replySubmitApi = (params: any) => {
|
|||||||
reply_id: params.reply_id,
|
reply_id: params.reply_id,
|
||||||
reply_user: params.reply_user,
|
reply_user: params.reply_user,
|
||||||
author: params.author,
|
author: params.author,
|
||||||
|
point: params.point,
|
||||||
|
key: params.key,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reply-submit',
|
name: 'reply-submit',
|
||||||
@@ -52,6 +56,8 @@ export const commentListApi = (params: any) => {
|
|||||||
page: params.page,
|
page: params.page,
|
||||||
size: params.size,
|
size: params.size,
|
||||||
topic_id: params.topic_id,
|
topic_id: params.topic_id,
|
||||||
|
user_id: params.user_id,
|
||||||
|
is_hot: params.is_hot,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cacheFor: {
|
cacheFor: {
|
||||||
@@ -75,6 +81,7 @@ export const replyListApi = (params: any) => {
|
|||||||
size: params.size,
|
size: params.size,
|
||||||
comment_id: params.comment_id,
|
comment_id: params.comment_id,
|
||||||
topic_id: params.topic_id,
|
topic_id: params.topic_id,
|
||||||
|
user_id: params.user_id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cacheFor: {
|
cacheFor: {
|
||||||
@@ -104,6 +111,8 @@ export const replyReplySubmitApi = (params: any) => {
|
|||||||
reply_id: params.reply_id,
|
reply_id: params.reply_id,
|
||||||
reply_user: params.reply_user,
|
reply_user: params.reply_user,
|
||||||
author: params.author,
|
author: params.author,
|
||||||
|
point: params.point,
|
||||||
|
key: params.key,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reply-reply-submit',
|
name: 'reply-reply-submit',
|
||||||
@@ -113,3 +122,38 @@ export const replyReplySubmitApi = (params: any) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* @description 评论点赞
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
export const commentLikeApi = (params: any) => {
|
||||||
|
return service.Post('/api/auth/comment/like', {
|
||||||
|
user_id: params.user_id,
|
||||||
|
comment_id: params.comment_id,
|
||||||
|
topic_id: params.topic_id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
ignoreToken: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 评论取消点赞
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
export const cancelCommentLikeApi = (params: any) => {
|
||||||
|
return service.Post('/api/auth/comment/cancel_like', {
|
||||||
|
user_id: params.user_id,
|
||||||
|
comment_id: params.comment_id,
|
||||||
|
topic_id: params.topic_id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
ignoreToken: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@@ -1,879 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="comment-main">
|
|
||||||
<AFlex :vertical="false" justify="flex-start" class="comment-header">
|
|
||||||
<span class="comment-header-title">{{ t('comment.comment') }}</span>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 评论输入框 -->
|
|
||||||
<div class="comment">
|
|
||||||
<AFlex :vertical="false">
|
|
||||||
<AFlex :vertical="true">
|
|
||||||
<AAvatar :size="50" shape="circle" src="https://api.multiavatar.com/Starcrasher.svg"/>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="true" class="comment-content">
|
|
||||||
<ATextarea :rows="4" class="comment-text" @focus="onFocusHandler"
|
|
||||||
v-model:value="commentContent"
|
|
||||||
@keyup.ctrl.enter="commentSubmitThrottled"
|
|
||||||
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions"
|
|
||||||
v-if="showCommentActions">
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AFlex :vertical="false" align="center" class="comment-action-item">
|
|
||||||
<APopover trigger="click" placement="bottom">
|
|
||||||
<template #content>
|
|
||||||
<div style="width: 170px;height: 200px;overflow: auto;">
|
|
||||||
<template v-for="(item) in EMOJI" :key="item">
|
|
||||||
<AButton @click="insertEmoji(item)" type="text" size="large">{{ item }}</AButton>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<AButton type="text" size="small" :icon="h(SmileOutlined)" class="comment-action-icon">
|
|
||||||
{{ t('comment.emoji') }}
|
|
||||||
</AButton>
|
|
||||||
</APopover>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center" class="comment-action-item">
|
|
||||||
<AUpload
|
|
||||||
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
|
||||||
name="images"
|
|
||||||
:max-count="3"
|
|
||||||
:multiple="true"
|
|
||||||
method="post"
|
|
||||||
:directory="false"
|
|
||||||
:show-upload-list="false"
|
|
||||||
:custom-request="customUploadRequest"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
:disabled="imageList.length >= 3"
|
|
||||||
>
|
|
||||||
<ABadge :count="imageList.length">
|
|
||||||
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
|
||||||
class="comment-action-icon">
|
|
||||||
{{ t('comment.picture') }}
|
|
||||||
</AButton>
|
|
||||||
</ABadge>
|
|
||||||
</AUpload>
|
|
||||||
<template v-if="imageList.length > 0">
|
|
||||||
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index">
|
|
||||||
<template #count>
|
|
||||||
<CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/>
|
|
||||||
</template>
|
|
||||||
<AAvatar shape="square" size="small">
|
|
||||||
<template #icon>
|
|
||||||
<AImage v-if="item" :width="24" :height="24" :src="item"/>
|
|
||||||
</template>
|
|
||||||
</AAvatar>
|
|
||||||
</ABadge>
|
|
||||||
</template>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton
|
|
||||||
@click="commentSubmitThrottled"
|
|
||||||
:disabled="commentContent.trim().length === 0"
|
|
||||||
type="primary" size="middle" class="comment-action-btn">{{ t('comment.sendComment') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</div>
|
|
||||||
<ADivider/>
|
|
||||||
<!-- 回复列表 -->
|
|
||||||
<div class="reply">
|
|
||||||
<div class="reply-header">
|
|
||||||
<!-- 评论列表头部 -->
|
|
||||||
<AFlex :vertical="true">
|
|
||||||
<AFlex :vertical="false">
|
|
||||||
<span class="reply-header-title">{{ t('comment.allComments') }}</span>
|
|
||||||
<span class="reply-header-count">123</span>
|
|
||||||
</AFlex>
|
|
||||||
<ASegmented v-model:value="segmentedValue" :options="data" class="reply-header-sort"/>
|
|
||||||
</AFlex>
|
|
||||||
</div>
|
|
||||||
<ASkeleton :loading="commentLoading" avatar active :paragraph="{ rows: 4 }"
|
|
||||||
>
|
|
||||||
<div class="reply-list" v-if="commentList?.comments">
|
|
||||||
<div class="reply-item" v-for="(item, index) in commentList?.comments" :key="index">
|
|
||||||
<AFlex :vertical="false" style="margin-top: 5px">
|
|
||||||
<!-- 评论头像 -->
|
|
||||||
<AFlex :vertical="true" class="reply-avatar" v-if="item.avatar">
|
|
||||||
<AAvatar :size="50" shape="circle" :src="item.avatar"/>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 评论内容 -->
|
|
||||||
<AFlex :vertical="true" class="reply-content">
|
|
||||||
<AFlex :vertical="true">
|
|
||||||
<AFlex :vertical="false" align="flex-start">
|
|
||||||
<span class="reply-name">{{ item.nickname }}</span>
|
|
||||||
<a-tag color="cyan" class="reply-tag" size="small">Lv.5</a-tag>
|
|
||||||
<a-tag color="red" class="reply-tag" size="small" v-if="item.author===1">UP</a-tag>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="flex-end" justify="space-between">
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between">
|
|
||||||
<span class="reply-ip"> {{ item.location }} </span>
|
|
||||||
</AFlex>
|
|
||||||
<span class="reply-time">{{ formatTimeAgo(item.created_time) }}</span>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
|
|
||||||
<ACard class="reply-card" :body-style="{padding: '10px'}">
|
|
||||||
<div class="reply-text" v-html="item.content">
|
|
||||||
</div>
|
|
||||||
<AFlex :vertical="false" align="center" class="reply-images" v-if="item.images">
|
|
||||||
<AAvatar shape="square" size="large"
|
|
||||||
v-for="(image, index) in item.images" :key="index">
|
|
||||||
<template #icon>
|
|
||||||
<AImage :width="40" :height="40" :src="image">
|
|
||||||
<template #previewMask>
|
|
||||||
<EyeOutlined style="font-size: 18px;"/>
|
|
||||||
</template>
|
|
||||||
</AImage>
|
|
||||||
</template>
|
|
||||||
</AAvatar>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" justify="space-between" align="center">
|
|
||||||
<!--评论操作按钮 -->
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between" class="reply-action-item">
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton type="text" size="small" :icon="h(LikeOutlined)" class="reply-action-btn">
|
|
||||||
{{ item.likes }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton type="text" size="small" :icon="h(DislikeOutlined)" class="reply-action-btn">
|
|
||||||
{{ item.dislikes }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<AButton @click="()=>{
|
|
||||||
handleShowReplyComment(item.id);
|
|
||||||
replyListThrottled(item.id)}" type="text" size="small"
|
|
||||||
:icon="h(MessageOutlined)"
|
|
||||||
class="reply-action-btn">
|
|
||||||
{{ item.reply_count }}
|
|
||||||
</AButton>
|
|
||||||
<AButton
|
|
||||||
@click="handleShowReplyInput(item.id)"
|
|
||||||
type="text" size="small" :icon="h(CommentOutlined)"
|
|
||||||
class="reply-action-btn">
|
|
||||||
{{ t('comment.reply') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 评论操作系统信息-->
|
|
||||||
<AFlex :vertical="false" align="center" justify="flex-end" class="reply-action-item-right">
|
|
||||||
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)" class="reply-action-info">
|
|
||||||
{{ item.operating_system }}
|
|
||||||
</AButton>
|
|
||||||
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)" class="reply-action-info">
|
|
||||||
{{ item.browser }}
|
|
||||||
</AButton>
|
|
||||||
<!-- 评论操作按钮 -->
|
|
||||||
<ADropdown trigger="click">
|
|
||||||
<AButton type="text" size="small" :icon="h(EllipsisOutlined)" class="reply-action-btn"
|
|
||||||
@click.prevent>
|
|
||||||
</AButton>
|
|
||||||
<template #overlay>
|
|
||||||
<AMenu>
|
|
||||||
<AMenuItem key="report">
|
|
||||||
<WarningOutlined/>
|
|
||||||
{{ t('comment.report') }}
|
|
||||||
</AMenuItem>
|
|
||||||
<AMenuItem key="copy">
|
|
||||||
<CopyOutlined/>
|
|
||||||
{{ t('comment.copy') }}
|
|
||||||
</AMenuItem>
|
|
||||||
<AMenuItem key="delete">
|
|
||||||
<DeleteOutlined/>
|
|
||||||
{{ t('comment.delete') }}
|
|
||||||
</AMenuItem>
|
|
||||||
</AMenu>
|
|
||||||
</template>
|
|
||||||
</ADropdown>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</ACard>
|
|
||||||
|
|
||||||
</AFlex>
|
|
||||||
<!-- 回复输入框 -->
|
|
||||||
<AFlex :vertical="true" class="reply-input-main" v-if="showReplyInput && item.id === showReplyInput">
|
|
||||||
<AFlex :vertical="false" align="center" class="reply-input-header">
|
|
||||||
<span class="reply-input-title">{{ t('comment.reply') + ':' }}</span>
|
|
||||||
<span class="reply-input-author">{{ item.nickname }}</span>
|
|
||||||
<AButton @click="closeReplyInput" type="dashed" size="small" :icon="h(CloseOutlined )"
|
|
||||||
class="reply-input-cancel">
|
|
||||||
{{ t('comment.cancelReply') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 回复头像-->
|
|
||||||
<AFlex :vertical="false" class="reply-input-content">
|
|
||||||
<AFlex :vertical="true" class="reply-input-avatar">
|
|
||||||
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 评论输入框 -->
|
|
||||||
<AFlex :vertical="true" class="reply-input-content-text">
|
|
||||||
<ATextarea :rows="3" class="comment-text-reply"
|
|
||||||
v-model:value="replyContent"
|
|
||||||
@keyup.ctrl.enter="()=>{
|
|
||||||
const params: any ={
|
|
||||||
reply_id: item.id,
|
|
||||||
reply_user: item.user_id
|
|
||||||
}
|
|
||||||
replySubmitThrottled(params);
|
|
||||||
}"
|
|
||||||
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions-reply"
|
|
||||||
>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply">
|
|
||||||
<APopover trigger="click" placement="bottom">
|
|
||||||
<template #content>
|
|
||||||
<div style="width: 170px;height: 200px;overflow: auto;">
|
|
||||||
<template v-for="(emoji) in EMOJI" :key="emoji">
|
|
||||||
<AButton @click="insertEmojiToReplyContent(emoji)" type="text" size="large">{{
|
|
||||||
emoji
|
|
||||||
}}
|
|
||||||
</AButton>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<AButton type="text" size="small" :icon="h(SmileOutlined)"
|
|
||||||
class="comment-action-icon-reply">
|
|
||||||
{{ t('comment.emoji') }}
|
|
||||||
</AButton>
|
|
||||||
</APopover>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply">
|
|
||||||
<AUpload
|
|
||||||
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
|
||||||
name="images"
|
|
||||||
:max-count="3"
|
|
||||||
:multiple="true"
|
|
||||||
method="post"
|
|
||||||
:directory="false"
|
|
||||||
:show-upload-list="false"
|
|
||||||
:custom-request="customUploadRequest"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
:disabled="imageList.length >= 3"
|
|
||||||
>
|
|
||||||
<ABadge :count="imageList.length">
|
|
||||||
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
|
||||||
class="comment-action-icon-reply">
|
|
||||||
{{ t('comment.picture') }}
|
|
||||||
</AButton>
|
|
||||||
</ABadge>
|
|
||||||
</AUpload>
|
|
||||||
<template v-if="imageList.length > 0">
|
|
||||||
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index">
|
|
||||||
<template #count>
|
|
||||||
<CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/>
|
|
||||||
</template>
|
|
||||||
<AAvatar shape="square" size="small">
|
|
||||||
<template #icon>
|
|
||||||
<AImage v-if="item" :width="24" :height="24" :src="item"/>
|
|
||||||
</template>
|
|
||||||
</AAvatar>
|
|
||||||
</ABadge>
|
|
||||||
</template>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton @click="()=>{
|
|
||||||
const params: any ={
|
|
||||||
reply_id: item.id,
|
|
||||||
reply_user: item.user_id
|
|
||||||
}
|
|
||||||
replySubmitThrottled(params);
|
|
||||||
}" type="primary" size="middle"
|
|
||||||
:disabled="replyContent.trim().length === 0"
|
|
||||||
class="comment-action-btn-reply">
|
|
||||||
{{ t('comment.sendComment') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
|
|
||||||
<!-- 子回复列表 -->
|
|
||||||
|
|
||||||
<AFlex :vertical="true" class="reply-item-child"
|
|
||||||
v-if="showReplyComment && showReplyComment === item.id">
|
|
||||||
<ASpin :spinning="replyLoading" size="default">
|
|
||||||
<AFlex :vertical="true" v-if="replyList.comments">
|
|
||||||
<AFlex :vertical="false" style="margin-top: 5px" v-for="(child, index) in replyList.comments"
|
|
||||||
:key="index">
|
|
||||||
<AFlex :vertical="true" class="reply-item-child-avatar">
|
|
||||||
<AAvatar :size="40" shape="circle" :src="child.avatar"/>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="true" class="reply-item-child-content">
|
|
||||||
<AFlex :vertical="true">
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<span class="reply-name-child">{{ child.nickname }}</span>
|
|
||||||
<span
|
|
||||||
class="reply-at">@{{ child.reply_username }}</span>
|
|
||||||
<a-tag color="cyan" class="reply-tag-child" size="small">Lv.5</a-tag>
|
|
||||||
<!-- <a-tag color="red" class="reply-tag" size="small">UP</a-tag>-->
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="flex-end" justify="space-between">
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between">
|
|
||||||
<span class="reply-ip-child"> {{ child.location }} </span>
|
|
||||||
</AFlex>
|
|
||||||
<span class="reply-time-child">{{ formatTimeAgo(child.created_time) }}</span>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="true" align="center">
|
|
||||||
<ACard class="reply-card-child" :body-style="{padding: '10px'}">
|
|
||||||
<div class="reply-text-child" v-html="child.content">
|
|
||||||
</div>
|
|
||||||
<AFlex :vertical="false" align="center" class="reply-images" v-if="child.images">
|
|
||||||
<AAvatar shape="square" size="large"
|
|
||||||
v-for="(image, index) in child.images" :key="index">
|
|
||||||
<template #icon>
|
|
||||||
<AImage :width="40" :height="40" :src="image">
|
|
||||||
<template #previewMask>
|
|
||||||
<EyeOutlined style="font-size: 18px;"/>
|
|
||||||
</template>
|
|
||||||
</AImage>
|
|
||||||
</template>
|
|
||||||
</AAvatar>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" justify="space-between" align="center">
|
|
||||||
<!--评论操作按钮 -->
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between"
|
|
||||||
class="reply-action-item-child">
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton type="text" size="small" :icon="h(LikeOutlined)"
|
|
||||||
class="reply-action-btn-child">
|
|
||||||
{{ child.likes }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton type="text" size="small" :icon="h(DislikeOutlined)"
|
|
||||||
class="reply-action-btn-child">
|
|
||||||
{{ child.dislikes }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<AButton
|
|
||||||
@click="handleShowReplyInput(child.id)"
|
|
||||||
type="text" size="small" :icon="h(CommentOutlined)"
|
|
||||||
class="reply-action-btn-child">
|
|
||||||
{{ t('comment.reply') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 评论操作系统信息-->
|
|
||||||
<AFlex :vertical="false" align="center" justify="flex-end"
|
|
||||||
class="reply-action-item-right-child">
|
|
||||||
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)"
|
|
||||||
class="reply-action-info-child">
|
|
||||||
{{ child.operating_system }}
|
|
||||||
</AButton>
|
|
||||||
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)"
|
|
||||||
class="reply-action-info-child">
|
|
||||||
{{ child.browser }}
|
|
||||||
</AButton>
|
|
||||||
<!-- 评论操作按钮 -->
|
|
||||||
<ADropdown trigger="click">
|
|
||||||
<AButton type="text" size="small" :icon="h(EllipsisOutlined)"
|
|
||||||
class="reply-action-btn-child"
|
|
||||||
@click.prevent>
|
|
||||||
</AButton>
|
|
||||||
<template #overlay>
|
|
||||||
<AMenu>
|
|
||||||
<AMenuItem key="report">
|
|
||||||
{{ t('comment.report') }}
|
|
||||||
</AMenuItem>
|
|
||||||
<AMenuItem key="copy">
|
|
||||||
{{ t('comment.copy') }}
|
|
||||||
</AMenuItem>
|
|
||||||
<AMenuItem key="delete">
|
|
||||||
{{ t('comment.delete') }}
|
|
||||||
</AMenuItem>
|
|
||||||
</AMenu>
|
|
||||||
</template>
|
|
||||||
</ADropdown>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</ACard>
|
|
||||||
|
|
||||||
<!-- 子评论回复输入框 -->
|
|
||||||
<AFlex :vertical="true" class="reply-input-main-child"
|
|
||||||
v-if="showReplyInput && child.id === showReplyInput">
|
|
||||||
<AFlex :vertical="false" align="center" class="reply-input-header-child">
|
|
||||||
<span class="reply-input-title-child">{{ t('comment.reply') + ':' }}</span>
|
|
||||||
<span class="reply-input-author-child">{{ child.nickname }}</span>
|
|
||||||
<AButton @click="closeReplyInput" type="dashed" size="small"
|
|
||||||
:icon="h(CloseOutlined )"
|
|
||||||
class="reply-input-cancel-child">
|
|
||||||
{{ t('comment.cancelReply') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 回复头像-->
|
|
||||||
<AFlex :vertical="false" class="reply-input-content-child">
|
|
||||||
<AFlex :vertical="true" class="reply-input-avatar-child">
|
|
||||||
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
|
||||||
</AFlex>
|
|
||||||
<!-- 评论输入框 -->
|
|
||||||
<AFlex :vertical="true" class="reply-input-content-text-child">
|
|
||||||
<ATextarea :rows="3" class="comment-text-reply-child"
|
|
||||||
v-model:value="replyReplyContent"
|
|
||||||
@keyup.ctrl.enter="()=>{
|
|
||||||
const params: any ={
|
|
||||||
reply_to: child.id,
|
|
||||||
reply_id: item.id,
|
|
||||||
reply_user: child.user_id
|
|
||||||
};
|
|
||||||
replyReplySubmitThrottled(params);
|
|
||||||
}"
|
|
||||||
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
|
||||||
<AFlex :vertical="false" align="center" justify="space-between"
|
|
||||||
class="comment-actions-reply-child"
|
|
||||||
>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
|
|
||||||
<APopover trigger="click" placement="bottom">
|
|
||||||
<template #content>
|
|
||||||
<div style="width: 170px;height: 200px;overflow: auto;">
|
|
||||||
<template v-for="(emoji) in EMOJI" :key="emoji">
|
|
||||||
<AButton @click="insertEmojiToReplyReplyContent(emoji)" type="text"
|
|
||||||
size="large">{{
|
|
||||||
emoji
|
|
||||||
}}
|
|
||||||
</AButton>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<AButton type="text" size="small" :icon="h(SmileOutlined)"
|
|
||||||
class="comment-action-icon-reply-child">
|
|
||||||
{{ t('comment.emoji') }}
|
|
||||||
</AButton>
|
|
||||||
</APopover>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
|
|
||||||
<AUpload
|
|
||||||
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
|
||||||
name="images"
|
|
||||||
:max-count="3"
|
|
||||||
:multiple="true"
|
|
||||||
method="post"
|
|
||||||
:directory="false"
|
|
||||||
:show-upload-list="false"
|
|
||||||
:custom-request="customUploadRequest"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
:disabled="imageList.length >= 3"
|
|
||||||
>
|
|
||||||
<ABadge :count="imageList.length">
|
|
||||||
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
|
||||||
class="comment-action-icon-reply-child">
|
|
||||||
{{ t('comment.picture') }}
|
|
||||||
</AButton>
|
|
||||||
</ABadge>
|
|
||||||
</AUpload>
|
|
||||||
<template v-if="imageList.length > 0">
|
|
||||||
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList"
|
|
||||||
:key="index">
|
|
||||||
<template #count>
|
|
||||||
<CloseCircleOutlined @click="removeBase64Image(index)"
|
|
||||||
style="color: #f5222d"/>
|
|
||||||
</template>
|
|
||||||
<AAvatar shape="square" size="small">
|
|
||||||
<template #icon>
|
|
||||||
<AImage v-if="item" :width="24" :height="24" :src="item"/>
|
|
||||||
</template>
|
|
||||||
</AAvatar>
|
|
||||||
</ABadge>
|
|
||||||
</template>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center">
|
|
||||||
<AButton
|
|
||||||
@click="()=>{
|
|
||||||
const params: any ={
|
|
||||||
reply_to: child.id,
|
|
||||||
reply_id: item.id,
|
|
||||||
reply_user: child.user_id
|
|
||||||
};
|
|
||||||
replyReplySubmitThrottled(params);
|
|
||||||
}"
|
|
||||||
:disabled="replyReplyContent.trim().length === 0" type="primary" size="middle"
|
|
||||||
class="comment-action-btn-reply-child">
|
|
||||||
{{ t('comment.sendComment') }}
|
|
||||||
</AButton>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
<APagination class="reply-pagination-child" size="small" :total="replyList.total"
|
|
||||||
:current="replyList.current" :page-size="replyList.size"
|
|
||||||
:default-page-size="replyList.size"
|
|
||||||
@change="(page: number, pageSize: number)=>{
|
|
||||||
getReplyList(item.id, page, pageSize);}"/>
|
|
||||||
</AFlex>
|
|
||||||
</ASpin>
|
|
||||||
</AFlex>
|
|
||||||
|
|
||||||
|
|
||||||
</AFlex>
|
|
||||||
|
|
||||||
|
|
||||||
</AFlex>
|
|
||||||
</div>
|
|
||||||
<APagination class="reply-pagination" @change="(page: number, pageSize: number)=>{
|
|
||||||
getCommentList(page, pageSize);
|
|
||||||
}" :current="commentList.current" :page-size="commentList.size" :total="commentList.total"
|
|
||||||
:default-page-size="commentList.size"
|
|
||||||
:show-less-items="true"/>
|
|
||||||
</div>
|
|
||||||
</ASkeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import {useI18n} from "vue-i18n";
|
|
||||||
import {h, onMounted, reactive, ref} from "vue";
|
|
||||||
import {
|
|
||||||
ChromeOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
CommentOutlined,
|
|
||||||
DislikeOutlined,
|
|
||||||
EllipsisOutlined,
|
|
||||||
LikeOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
PictureOutlined,
|
|
||||||
SmileOutlined,
|
|
||||||
WindowsOutlined
|
|
||||||
} from "@ant-design/icons-vue";
|
|
||||||
import EMOJI from "@/constant/emoji.ts";
|
|
||||||
import imageCompression from "browser-image-compression";
|
|
||||||
import {message} from "ant-design-vue";
|
|
||||||
import {useThrottleFn} from "@vueuse/core";
|
|
||||||
import {commentListApi, commentSubmitApi, replyListApi, replyReplySubmitApi, replySubmitApi} from "@/api/comment";
|
|
||||||
import useStore from "@/store";
|
|
||||||
import {Comment, ReplyCommentParams} from "@/types/comment";
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
|
||||||
const showCommentActions = ref<boolean>(false);
|
|
||||||
const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder'));
|
|
||||||
const data = reactive([t('comment.latest'), t('comment.hot')]);
|
|
||||||
const segmentedValue = ref<string>(data[0]);
|
|
||||||
const commentContent = ref<string>("");
|
|
||||||
const replyContent = ref<string>("");
|
|
||||||
const replyReplyContent = ref<string>("");
|
|
||||||
const fileList = ref<any[]>([]);
|
|
||||||
const imageList = ref<any[]>([]);
|
|
||||||
const user = useStore().user;
|
|
||||||
const commentList = ref<Comment>({} as Comment);
|
|
||||||
const showReplyInput = ref<number | null>(null);
|
|
||||||
const showReplyComment = ref<number | null>(null);
|
|
||||||
const replyList = ref<Comment>({} as Comment);
|
|
||||||
const commentLoading = ref<boolean>(true);
|
|
||||||
const replyLoading = ref<boolean>(true);
|
|
||||||
const topicId = ref<string>("123");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 聚焦事件
|
|
||||||
*/
|
|
||||||
async function onFocusHandler() {
|
|
||||||
showCommentActions.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入表情
|
|
||||||
* @param emoji
|
|
||||||
*/
|
|
||||||
async function insertEmoji(emoji: string) {
|
|
||||||
commentContent.value += emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入表情到回复内容
|
|
||||||
* @param emoji
|
|
||||||
*/
|
|
||||||
async function insertEmojiToReplyContent(emoji: string) {
|
|
||||||
replyContent.value += emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入表情到回复内容
|
|
||||||
* @param emoji
|
|
||||||
*/
|
|
||||||
async function insertEmojiToReplyReplyContent(emoji: string) {
|
|
||||||
replyReplyContent.value += emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 压缩图片配置
|
|
||||||
const options = {
|
|
||||||
maxSizeMB: 0.4,
|
|
||||||
maxWidthOrHeight: 750,
|
|
||||||
maxIteration: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传文件前置
|
|
||||||
* @param file
|
|
||||||
*/
|
|
||||||
async function beforeUpload(file: any) {
|
|
||||||
if (!window.FileReader) return false; // 判断是否支持FileReader
|
|
||||||
const compressedFile = await imageCompression(file, options);
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(compressedFile); // 文件转换
|
|
||||||
reader.onloadend = async function () {
|
|
||||||
if (fileList.value.length >= 5) {
|
|
||||||
message.error("最多只能上传5张图片");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
fileList.value.push(reader.result);
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义上传图片请求
|
|
||||||
*/
|
|
||||||
async function customUploadRequest() {
|
|
||||||
imageList.value = fileList.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除图片
|
|
||||||
* @param index
|
|
||||||
*/
|
|
||||||
async function removeBase64Image(index: number) {
|
|
||||||
fileList.value.splice(index, 1);
|
|
||||||
imageList.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评论提交 throttled
|
|
||||||
*/
|
|
||||||
const commentSubmitThrottled = useThrottleFn(commentSubmit, 1000);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评论提交
|
|
||||||
*/
|
|
||||||
async function commentSubmit() {
|
|
||||||
if (commentContent.value.trim() === "") {
|
|
||||||
message.error("评论内容不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (imageList.value.length > 3) {
|
|
||||||
message.error("最多只能上传3张图片");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const content = commentContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
|
||||||
|
|
||||||
const commentParams: object = {
|
|
||||||
user_id: user.user.uid,
|
|
||||||
topic_id: topicId.value,
|
|
||||||
content: content,
|
|
||||||
images: imageList.value,
|
|
||||||
author: user.user.uid,
|
|
||||||
};
|
|
||||||
const result: any = await commentSubmitApi(commentParams);
|
|
||||||
if (result.code === 200 && result.success) {
|
|
||||||
message.success("评论成功");
|
|
||||||
commentContent.value = "";
|
|
||||||
fileList.value = [];
|
|
||||||
imageList.value = [];
|
|
||||||
commentList.value = {} as Comment;
|
|
||||||
commentLoading.value = true;
|
|
||||||
await getCommentList();
|
|
||||||
} else {
|
|
||||||
message.error("评论失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回复提交 throttled
|
|
||||||
*/
|
|
||||||
const replySubmitThrottled = useThrottleFn(replySubmit, 1000);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回复提交
|
|
||||||
*/
|
|
||||||
async function replySubmit(data: ReplyCommentParams) {
|
|
||||||
if (replyContent.value.trim() === "") {
|
|
||||||
message.error("回复内容不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (imageList.value.length > 3) {
|
|
||||||
message.error("最多只能上传3张图片");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const content = replyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
|
||||||
|
|
||||||
const replyParams: {
|
|
||||||
images: any;
|
|
||||||
reply_id: number;
|
|
||||||
reply_user: string;
|
|
||||||
user_id: any;
|
|
||||||
author: any;
|
|
||||||
topic_id: string;
|
|
||||||
content: any
|
|
||||||
} = {
|
|
||||||
user_id: user.user.uid,
|
|
||||||
topic_id: topicId.value,
|
|
||||||
content: content,
|
|
||||||
images: imageList.value,
|
|
||||||
author: user.user.uid,
|
|
||||||
reply_id: data.reply_id,
|
|
||||||
reply_user: data.reply_user,
|
|
||||||
};
|
|
||||||
const result: any = await replySubmitApi(replyParams);
|
|
||||||
if (result.code === 200 && result.success) {
|
|
||||||
message.success("回复成功");
|
|
||||||
replyContent.value = "";
|
|
||||||
fileList.value = [];
|
|
||||||
imageList.value = [];
|
|
||||||
replyList.value = {} as Comment;
|
|
||||||
replyLoading.value = true;
|
|
||||||
await getReplyList(data.reply_id);
|
|
||||||
} else {
|
|
||||||
message.error("回复失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取评论列表
|
|
||||||
*/
|
|
||||||
async function getCommentList(page: number = 1, size: number = 5) {
|
|
||||||
const params = {
|
|
||||||
topic_id: topicId.value,
|
|
||||||
page: page,
|
|
||||||
size: size,
|
|
||||||
};
|
|
||||||
commentLoading.value = true;
|
|
||||||
commentList.value = {} as Comment;
|
|
||||||
// 获取评论列表
|
|
||||||
const result: any = await commentListApi(params);
|
|
||||||
if (result.code === 200 && result.success && result.data) {
|
|
||||||
commentList.value = result.data;
|
|
||||||
commentLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化时间
|
|
||||||
* @param dateString
|
|
||||||
*/
|
|
||||||
function formatTimeAgo(dateString: string) {
|
|
||||||
const now: any = new Date();
|
|
||||||
const date: any = new Date(dateString);
|
|
||||||
const seconds = Math.floor((now - date) / 1000);
|
|
||||||
|
|
||||||
const intervals = [
|
|
||||||
{label: '年', seconds: 31536000},
|
|
||||||
{label: '个月', seconds: 2592000},
|
|
||||||
{label: '天', seconds: 86400},
|
|
||||||
{label: '小时', seconds: 3600},
|
|
||||||
{label: '分钟', seconds: 60}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const interval of intervals) {
|
|
||||||
const count = Math.floor(seconds / interval.seconds);
|
|
||||||
if (count > 0) {
|
|
||||||
return `${count} ${interval.label}前`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${seconds} 秒前`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示回复输入框
|
|
||||||
*/
|
|
||||||
const handleShowReplyInput = (index: any) => {
|
|
||||||
showReplyInput.value = showReplyInput.value === index ? null : index;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 显示回复
|
|
||||||
*/
|
|
||||||
const handleShowReplyComment = (index: any) => {
|
|
||||||
showReplyComment.value = showReplyComment.value === index ? null : index;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 关闭回复输入框
|
|
||||||
*/
|
|
||||||
const closeReplyInput = () => {
|
|
||||||
showReplyInput.value = null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 获取回复列表 throttled
|
|
||||||
*/
|
|
||||||
const replyListThrottled = useThrottleFn(getReplyList, 500);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取回复列表
|
|
||||||
* @param reply_id
|
|
||||||
* @param page
|
|
||||||
* @param size
|
|
||||||
*/
|
|
||||||
async function getReplyList(reply_id: number, page: number = 1, size: number = 5) {
|
|
||||||
const params: any = {
|
|
||||||
topic_id: topicId.value,
|
|
||||||
page: page,
|
|
||||||
size: size,
|
|
||||||
comment_id: reply_id,
|
|
||||||
};
|
|
||||||
replyLoading.value = true;
|
|
||||||
replyList.value = {} as Comment;
|
|
||||||
// 获取评论列表
|
|
||||||
const result: any = await replyListApi(params);
|
|
||||||
if (result.code === 200 && result.success && result.data) {
|
|
||||||
replyList.value = result.data;
|
|
||||||
replyLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回复提交 throttled
|
|
||||||
*/
|
|
||||||
const replyReplySubmitThrottled = useThrottleFn(replyReplySubmit, 1000);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回复评论提交
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
async function replyReplySubmit(data: any) {
|
|
||||||
if (replyReplyContent.value.trim() === "") {
|
|
||||||
message.error("回复内容不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (imageList.value.length > 3) {
|
|
||||||
message.error("最多只能上传3张图片");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const content = replyReplyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
|
||||||
|
|
||||||
const replyParams: ReplyCommentParams = {
|
|
||||||
user_id: user.user.uid,
|
|
||||||
topic_id: topicId.value,
|
|
||||||
content: content,
|
|
||||||
images: imageList.value,
|
|
||||||
author: user.user.uid,
|
|
||||||
reply_to: data.reply_to,
|
|
||||||
reply_id: data.reply_id,
|
|
||||||
reply_user: data.reply_user,
|
|
||||||
};
|
|
||||||
const result: any = await replyReplySubmitApi(replyParams);
|
|
||||||
if (result.code === 200 && result.success) {
|
|
||||||
message.success("回复成功");
|
|
||||||
replyReplyContent.value = "";
|
|
||||||
fileList.value = [];
|
|
||||||
imageList.value = [];
|
|
||||||
} else {
|
|
||||||
message.error("回复失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getCommentList();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<style src="./index.scss" lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment">
|
||||||
|
<AFlex :vertical="false">
|
||||||
|
<AFlex :vertical="true">
|
||||||
|
<AAvatar :size="50" shape="circle" src="https://api.multiavatar.com/Starcrasher.svg"/>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="true" class="comment-content">
|
||||||
|
<ATextarea :rows="4" class="comment-text" @focus="onFocusHandler"
|
||||||
|
v-model:value="commentContent"
|
||||||
|
@keyup.ctrl.enter="showSlideCaptcha"
|
||||||
|
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions"
|
||||||
|
v-if="showCommentActions">
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AFlex :vertical="false" align="center" class="comment-action-item">
|
||||||
|
<APopover trigger="click" placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<div style="width: 170px;height: 200px;overflow: auto;">
|
||||||
|
<template v-for="(item) in EMOJI" :key="item">
|
||||||
|
<AButton @click="insertEmoji(item)" type="text" size="large">{{ item }}</AButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<AButton type="text" size="small" :icon="h(SmileOutlined)" class="comment-action-icon">
|
||||||
|
{{ t('comment.emoji') }}
|
||||||
|
</AButton>
|
||||||
|
</APopover>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center" class="comment-action-item">
|
||||||
|
<AUpload
|
||||||
|
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
||||||
|
name="images"
|
||||||
|
:max-count="3"
|
||||||
|
:multiple="true"
|
||||||
|
method="post"
|
||||||
|
:directory="false"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:custom-request="customUploadRequest"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:disabled="imageList.length >= 3"
|
||||||
|
>
|
||||||
|
<ABadge :count="imageList.length">
|
||||||
|
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
||||||
|
class="comment-action-icon">
|
||||||
|
{{ t('comment.picture') }}
|
||||||
|
</AButton>
|
||||||
|
</ABadge>
|
||||||
|
</AUpload>
|
||||||
|
<template v-if="imageList.length > 0">
|
||||||
|
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index">
|
||||||
|
<template #count>
|
||||||
|
<CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/>
|
||||||
|
</template>
|
||||||
|
<AAvatar shape="square" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<AImage v-if="item" :width="24" :height="24" :src="item"/>
|
||||||
|
</template>
|
||||||
|
</AAvatar>
|
||||||
|
</ABadge>
|
||||||
|
</template>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AButton
|
||||||
|
@click="showSlideCaptcha"
|
||||||
|
:disabled="commentContent.trim().length === 0"
|
||||||
|
type="primary" size="middle" class="comment-action-btn">{{ t('comment.sendComment') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AModal v-model:open="showSubmitCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||||
|
:maskClosable="false" :bodyStyle="{padding: 0}">
|
||||||
|
<gocaptcha-slide
|
||||||
|
:config="{}"
|
||||||
|
:data="comment.slideCaptchaData"
|
||||||
|
:events="commentSlideCaptchaEvent"
|
||||||
|
/>
|
||||||
|
</AModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import EMOJI from "@/constant/emoji.js";
|
||||||
|
import {h, ref} from "vue";
|
||||||
|
import {PictureOutlined, SmileOutlined} from "@ant-design/icons-vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import {commentSubmitApi} from "@/api/comment";
|
||||||
|
import {useDebounceFn, useThrottleFn} from "@vueuse/core";
|
||||||
|
import imageCompression from "browser-image-compression";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
|
const showCommentActions = ref<boolean>(false);
|
||||||
|
const commentContent = ref<string>("");
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
const imageList = ref<any[]>([]);
|
||||||
|
const user = useStore().user;
|
||||||
|
const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder'));
|
||||||
|
const topicId = ref<string>("123");
|
||||||
|
const showSubmitCaptcha = ref<boolean>(false);
|
||||||
|
const comment = useStore().comment;
|
||||||
|
const router = useRouter();
|
||||||
|
const commentSlideCaptchaEvent = {
|
||||||
|
confirm: async (point: any) => {
|
||||||
|
await commentSubmitDebounced(point);
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
showSubmitCaptcha.value = false;
|
||||||
|
},
|
||||||
|
refresh: () => {
|
||||||
|
getSlideCaptchaDataThrottled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聚焦事件
|
||||||
|
*/
|
||||||
|
async function onFocusHandler() {
|
||||||
|
showCommentActions.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入表情
|
||||||
|
* @param emoji
|
||||||
|
*/
|
||||||
|
async function insertEmoji(emoji: string) {
|
||||||
|
commentContent.value += emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论提交 debounce
|
||||||
|
*/
|
||||||
|
const commentSubmitDebounced = useDebounceFn(commentSubmit, 500);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论提交
|
||||||
|
*/
|
||||||
|
async function commentSubmit(point: any) {
|
||||||
|
if (commentContent.value.trim() === "") {
|
||||||
|
message.error(t('comment.commentContentNotEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (imageList.value.length > 3) {
|
||||||
|
message.error(t('comment.maxImageCount'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = commentContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
||||||
|
|
||||||
|
const commentParams: object = {
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
content: content,
|
||||||
|
images: imageList.value,
|
||||||
|
author: user.user.uid,
|
||||||
|
point: [point.x, point.y],
|
||||||
|
key: comment.slideCaptchaData.key,
|
||||||
|
};
|
||||||
|
const result: any = await commentSubmitApi(commentParams);
|
||||||
|
if (result.code === 200 && result.success) {
|
||||||
|
message.success(t('comment.commentSuccess'));
|
||||||
|
commentContent.value = "";
|
||||||
|
fileList.value = [];
|
||||||
|
imageList.value = [];
|
||||||
|
showSubmitCaptcha.value = false;
|
||||||
|
await getCommentList();
|
||||||
|
} else {
|
||||||
|
message.error(result.message || t('comment.commentError'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论列表
|
||||||
|
*/
|
||||||
|
async function getCommentList(page: number = 1, size: number = 5, hot: boolean = true) {
|
||||||
|
const params = {
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
is_hot: router.currentRoute.value.query.type === "hot" || hot,
|
||||||
|
};
|
||||||
|
await comment.getCommentList(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 压缩图片配置
|
||||||
|
const options = {
|
||||||
|
maxSizeMB: 0.4,
|
||||||
|
maxWidthOrHeight: 750,
|
||||||
|
maxIteration: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件前置
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
async function beforeUpload(file: any) {
|
||||||
|
if (!window.FileReader) return false; // 判断是否支持FileReader
|
||||||
|
const compressedFile = await imageCompression(file, options);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(compressedFile); // 文件转换
|
||||||
|
reader.onloadend = async function () {
|
||||||
|
if (fileList.value.length >= 5) {
|
||||||
|
message.error(t('comment.maxImageCount'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fileList.value.push(reader.result);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义上传图片请求
|
||||||
|
*/
|
||||||
|
async function customUploadRequest() {
|
||||||
|
imageList.value = fileList.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除图片
|
||||||
|
* @param index
|
||||||
|
*/
|
||||||
|
async function removeBase64Image(index: number) {
|
||||||
|
fileList.value.splice(index, 1);
|
||||||
|
imageList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSlideCaptchaDataThrottled = useThrottleFn(comment.getSlideCaptchaData, 1000);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示滑动验证码
|
||||||
|
*/
|
||||||
|
async function showSlideCaptcha() {
|
||||||
|
const res = await comment.getSlideCaptchaData();
|
||||||
|
if (res) {
|
||||||
|
showSubmitCaptcha.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="./index.scss" lang="scss" scoped>
|
||||||
|
</style>
|
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
.comment-content {
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
.comment-text {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.comment-action-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #767779;
|
||||||
|
|
||||||
|
|
||||||
|
.comment-action-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-action-icon:hover {
|
||||||
|
color: #08a327;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,325 @@
|
|||||||
|
<template>
|
||||||
|
<div class="reply">
|
||||||
|
<div class="reply-header">
|
||||||
|
<!-- 评论列表头部 -->
|
||||||
|
<AFlex :vertical="false" justify="space-between" align="center">
|
||||||
|
<AFlex :vertical="false">
|
||||||
|
<span class="reply-header-title">{{ t('comment.allComments') }}</span>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" justify="flex-end">
|
||||||
|
<AButton type="text" size="small" @click="getHotCommentList" :icon="h(FireOutlined)"
|
||||||
|
:style="{color: router.currentRoute.value.query.type === 'hot'? '#08a327' : '#000'}"
|
||||||
|
class="reply-header-hot">
|
||||||
|
{{ t('comment.hot') }}
|
||||||
|
</AButton>
|
||||||
|
<AButton type="text" size="small" @click="getLatestCommentList" :icon="h(ClockCircleOutlined)"
|
||||||
|
:style="{color: router.currentRoute.value.query.type === 'latest'? '#08a327' : '#000'}"
|
||||||
|
class="reply-header-latest">
|
||||||
|
{{ t('comment.latest') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</div>
|
||||||
|
<ASkeleton :loading="comment.commentLoading" avatar active :paragraph="{ rows: 4 }">
|
||||||
|
<div class="reply-list" v-if="comment.commentList">
|
||||||
|
<div class="reply-item" v-for="(item, index) in comment.commentList.comments" :key="index">
|
||||||
|
<AFlex :vertical="false" style="margin-top: 5px">
|
||||||
|
<!-- 评论头像 -->
|
||||||
|
<AFlex :vertical="true" class="reply-avatar" v-if="item.avatar">
|
||||||
|
<AAvatar :size="50" shape="circle" :src="item.avatar"/>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 评论内容 -->
|
||||||
|
<AFlex :vertical="true" class="reply-content">
|
||||||
|
<AFlex :vertical="true">
|
||||||
|
<AFlex :vertical="false" align="flex-start">
|
||||||
|
<span class="reply-name">{{ item.nickname }}</span>
|
||||||
|
<a-tag color="cyan" class="reply-tag" size="small">Lv.5</a-tag>
|
||||||
|
<a-tag color="red" class="reply-tag" size="small" v-if="item.author===1">UP</a-tag>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="flex-end" justify="space-between">
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between">
|
||||||
|
<span class="reply-ip"> {{ item.location }} </span>
|
||||||
|
</AFlex>
|
||||||
|
<span class="reply-time">{{ formatTimeAgo(item.created_time) }}</span>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
|
||||||
|
<ACard class="reply-card" :body-style="{padding: '10px'}">
|
||||||
|
<div class="reply-text" v-html="item.content">
|
||||||
|
</div>
|
||||||
|
<AFlex :vertical="false" align="center" class="reply-images" v-if="item.images">
|
||||||
|
<AAvatar shape="square" size="large"
|
||||||
|
v-for="(image, index) in item.images" :key="index">
|
||||||
|
<template #icon>
|
||||||
|
<AImage :width="40" :height="40" :src="image">
|
||||||
|
<template #previewMask>
|
||||||
|
<EyeOutlined style="font-size: 18px;"/>
|
||||||
|
</template>
|
||||||
|
</AImage>
|
||||||
|
</template>
|
||||||
|
</AAvatar>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" justify="space-between" align="center">
|
||||||
|
<!--评论操作按钮 -->
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between" class="reply-action-item">
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AButton v-if="!item.is_liked" @click="commentLikeThrottled(item)" type="text" size="small"
|
||||||
|
:icon="h(LikeOutlined)" class="reply-action-btn">
|
||||||
|
{{ item.likes }}
|
||||||
|
</AButton>
|
||||||
|
<AButton v-if="item.is_liked" @click="cancelCommentLikeThrottled(item)" type="text"
|
||||||
|
size="small"
|
||||||
|
:icon="h(LikeFilled)" style="color: red" class="reply-action-btn">
|
||||||
|
{{ item.likes }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
<AButton @click="()=>{
|
||||||
|
comment.handleShowCommentReply(item.id);
|
||||||
|
replyListThrottled(item.id)}" type="text" size="small"
|
||||||
|
:icon="h(MessageOutlined)"
|
||||||
|
class="reply-action-btn">
|
||||||
|
{{ item.reply_count }}
|
||||||
|
</AButton>
|
||||||
|
<AButton
|
||||||
|
@click="comment.handleShowReplyInput(item.id)"
|
||||||
|
type="text" size="small" :icon="h(CommentOutlined)"
|
||||||
|
class="reply-action-btn">
|
||||||
|
{{ t('comment.reply') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 评论操作系统信息-->
|
||||||
|
<AFlex :vertical="false" align="center" justify="flex-end" class="reply-action-item-right">
|
||||||
|
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)" class="reply-action-info">
|
||||||
|
{{ item.operating_system }}
|
||||||
|
</AButton>
|
||||||
|
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)" class="reply-action-info">
|
||||||
|
{{ item.browser }}
|
||||||
|
</AButton>
|
||||||
|
<!-- 评论操作按钮 -->
|
||||||
|
<ADropdown trigger="click">
|
||||||
|
<AButton type="text" size="small" :icon="h(EllipsisOutlined)" class="reply-action-btn"
|
||||||
|
@click.prevent>
|
||||||
|
</AButton>
|
||||||
|
<template #overlay>
|
||||||
|
<AMenu>
|
||||||
|
<AMenuItem key="report">
|
||||||
|
<WarningOutlined/>
|
||||||
|
{{ t('comment.report') }}
|
||||||
|
</AMenuItem>
|
||||||
|
<AMenuItem key="copy">
|
||||||
|
<CopyOutlined/>
|
||||||
|
{{ t('comment.copy') }}
|
||||||
|
</AMenuItem>
|
||||||
|
<AMenuItem key="delete">
|
||||||
|
<DeleteOutlined/>
|
||||||
|
{{ t('comment.delete') }}
|
||||||
|
</AMenuItem>
|
||||||
|
</AMenu>
|
||||||
|
</template>
|
||||||
|
</ADropdown>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</ACard>
|
||||||
|
|
||||||
|
</AFlex>
|
||||||
|
<!-- 回复输入框 -->
|
||||||
|
<ReplyInput :item="item" v-if="comment.showReplyInput && item.id === comment.showReplyInput"/>
|
||||||
|
<!-- 子回复列表 -->
|
||||||
|
<ReplyList :item="item" v-if="comment.showCommentReply && item.id === comment.showCommentReply"/>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</div>
|
||||||
|
<APagination v-if="comment.commentList.total > 0" class="reply-pagination" @change="paginationCommentChange"
|
||||||
|
:current="Number(router.currentRoute.value.query.page) || comment.commentList.current"
|
||||||
|
:page-size="comment.commentList.size" :total="comment.commentList.total"
|
||||||
|
:default-page-size="comment.commentList.size"
|
||||||
|
:show-less-items="true"/>
|
||||||
|
</div>
|
||||||
|
</ASkeleton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {h, onMounted, ref} from "vue";
|
||||||
|
import {
|
||||||
|
ChromeOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CommentOutlined,
|
||||||
|
EllipsisOutlined,
|
||||||
|
FireOutlined,
|
||||||
|
LikeFilled,
|
||||||
|
LikeOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
WindowsOutlined
|
||||||
|
} from "@ant-design/icons-vue";
|
||||||
|
import {useThrottleFn} from "@vueuse/core";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import ReplyInput from "@/components/CommentReply/Components/ReplyInput/ReplyInput.vue";
|
||||||
|
import ReplyList from "@/components/CommentReply/Components/ReplyList/ReplyList.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const comment = useStore().comment;
|
||||||
|
const user = useStore().user;
|
||||||
|
|
||||||
|
const topicId = ref<string>("123");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论列表
|
||||||
|
*/
|
||||||
|
async function getCommentList(page: number = 1, size: number = 5, hot: boolean = true) {
|
||||||
|
const params = {
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
is_hot: router.currentRoute.value.query.type === "hot" || hot,
|
||||||
|
};
|
||||||
|
await comment.getCommentList(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间
|
||||||
|
* @param dateString
|
||||||
|
*/
|
||||||
|
function formatTimeAgo(dateString: string) {
|
||||||
|
const now: any = new Date();
|
||||||
|
const date: any = new Date(dateString);
|
||||||
|
const seconds = Math.floor((now - date) / 1000);
|
||||||
|
|
||||||
|
const intervals = [
|
||||||
|
{label: '年', seconds: 31536000},
|
||||||
|
{label: '个月', seconds: 2592000},
|
||||||
|
{label: '天', seconds: 86400},
|
||||||
|
{label: '小时', seconds: 3600},
|
||||||
|
{label: '分钟', seconds: 60}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const interval of intervals) {
|
||||||
|
const count = Math.floor(seconds / interval.seconds);
|
||||||
|
if (count > 0) {
|
||||||
|
return `${count} ${interval.label}前`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${seconds} 秒前`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表 throttled
|
||||||
|
*/
|
||||||
|
const replyListThrottled = useThrottleFn(getReplyList, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表
|
||||||
|
* @param reply_id
|
||||||
|
* @param page
|
||||||
|
* @param size
|
||||||
|
*/
|
||||||
|
async function getReplyList(reply_id: number, page: number = 1, size: number = 5) {
|
||||||
|
const params: any = {
|
||||||
|
topic_id: topicId.value,
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
comment_id: reply_id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
};
|
||||||
|
await comment.getReplyList(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentLikeThrottled = useThrottleFn(commentLike, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论点赞
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
async function commentLike(item: any) {
|
||||||
|
const params: any = {
|
||||||
|
comment_id: item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
};
|
||||||
|
const res: boolean = await comment.commentLike(params);
|
||||||
|
if (res) {
|
||||||
|
item.is_liked = true;
|
||||||
|
item.likes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelCommentLikeThrottled = useThrottleFn(cancelCommentLike, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消评论点赞
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
async function cancelCommentLike(item: any) {
|
||||||
|
const params: any = {
|
||||||
|
comment_id: item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
};
|
||||||
|
const res: boolean = await comment.cancelCommentLike(params);
|
||||||
|
if (res) {
|
||||||
|
item.is_liked = false;
|
||||||
|
item.likes--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论分页器 change 事件
|
||||||
|
* @param page
|
||||||
|
* @param pageSize
|
||||||
|
*/
|
||||||
|
async function paginationCommentChange(page: number, pageSize: number) {
|
||||||
|
await router.push({
|
||||||
|
path: "/main",
|
||||||
|
query: {
|
||||||
|
type: router.currentRoute.value.query.type,
|
||||||
|
page: page,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await getCommentList(page, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门评论
|
||||||
|
*/
|
||||||
|
async function getHotCommentList() {
|
||||||
|
await getCommentList(1, 5, true);
|
||||||
|
await router.push({
|
||||||
|
path: "/main",
|
||||||
|
query: {
|
||||||
|
type: "hot",
|
||||||
|
page: router.currentRoute.value.query.page,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最新评论
|
||||||
|
*/
|
||||||
|
async function getLatestCommentList() {
|
||||||
|
await getCommentList(1, 5, false);
|
||||||
|
await router.push({
|
||||||
|
path: "/main",
|
||||||
|
query: {
|
||||||
|
type: "latest",
|
||||||
|
page: router.currentRoute.value.query.page,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const page = Number(router.currentRoute.value.query.page) || 1;
|
||||||
|
getCommentList(page);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./index.scss">
|
||||||
|
</style>
|
119
src/components/CommentReply/Components/CommentList/index.scss
Normal file
119
src/components/CommentReply/Components/CommentList/index.scss
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
.reply-header {
|
||||||
|
.reply-header-title {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-list {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.reply-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-content {
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
.reply-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-left: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-ip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-card {
|
||||||
|
width: 600px;
|
||||||
|
//margin-top: 5px;
|
||||||
|
|
||||||
|
.reply-images {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-item {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.reply-action-btn {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-icon:hover {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #08a327;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-btn:hover {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #08a327;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-icon-number {
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-item-right {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.reply-action-btn {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-btn:hover {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #08a327;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-info {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
269
src/components/CommentReply/Components/ReplyInput/ReplyInput.vue
Normal file
269
src/components/CommentReply/Components/ReplyInput/ReplyInput.vue
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<AFlex :vertical="true" class="reply-input-main">
|
||||||
|
<AFlex :vertical="false" align="center" class="reply-input-header">
|
||||||
|
<span class="reply-input-title">{{ t('comment.reply') + ':' }}</span>
|
||||||
|
<span class="reply-input-author">{{ props.item.nickname }}</span>
|
||||||
|
<AButton @click="comment.closeReplyInput" type="dashed" size="small" :icon="h(CloseOutlined )"
|
||||||
|
class="reply-input-cancel">
|
||||||
|
{{ t('comment.cancelReply') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 回复头像-->
|
||||||
|
<AFlex :vertical="false" class="reply-input-content">
|
||||||
|
<AFlex :vertical="true" class="reply-input-avatar">
|
||||||
|
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 评论输入框 -->
|
||||||
|
<AFlex :vertical="true" class="reply-input-content-text">
|
||||||
|
<ATextarea :rows="3" class="comment-text-reply"
|
||||||
|
v-model:value="replyContent"
|
||||||
|
@keyup.ctrl.enter="showSlideCaptcha"
|
||||||
|
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions-reply"
|
||||||
|
>
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AFlex :vertical="false" align="center" class="comment-action-item-reply">
|
||||||
|
<APopover trigger="click" placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<div style="width: 170px;height: 200px;overflow: auto;">
|
||||||
|
<template v-for="(emoji) in EMOJI" :key="emoji">
|
||||||
|
<AButton @click="insertEmojiToReplyContent(emoji)" type="text" size="large">
|
||||||
|
{{ emoji }}
|
||||||
|
</AButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<AButton type="text" size="small" :icon="h(SmileOutlined)"
|
||||||
|
class="comment-action-icon-reply">
|
||||||
|
{{ t('comment.emoji') }}
|
||||||
|
</AButton>
|
||||||
|
</APopover>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center" class="comment-action-item-reply">
|
||||||
|
<AUpload
|
||||||
|
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
||||||
|
name="images"
|
||||||
|
:max-count="3"
|
||||||
|
:multiple="true"
|
||||||
|
method="post"
|
||||||
|
:directory="false"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:custom-request="customUploadRequest"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:disabled="imageList.length >= 3"
|
||||||
|
>
|
||||||
|
<ABadge :count="imageList.length">
|
||||||
|
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
||||||
|
class="comment-action-icon-reply">
|
||||||
|
{{ t('comment.picture') }}
|
||||||
|
</AButton>
|
||||||
|
</ABadge>
|
||||||
|
</AUpload>
|
||||||
|
<template v-if="imageList.length > 0">
|
||||||
|
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index">
|
||||||
|
<template #count>
|
||||||
|
<CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/>
|
||||||
|
</template>
|
||||||
|
<AAvatar shape="square" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<AImage v-if="item" :width="24" :height="24" :src="item"/>
|
||||||
|
</template>
|
||||||
|
</AAvatar>
|
||||||
|
</ABadge>
|
||||||
|
</template>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AButton @click="showSlideCaptcha" type="primary" size="middle"
|
||||||
|
:disabled="replyContent.trim().length === 0"
|
||||||
|
class="comment-action-btn-reply">
|
||||||
|
{{ t('comment.sendComment') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
|
||||||
|
<AModal v-model:open="showSubmitCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||||
|
:maskClosable="false" :bodyStyle="{padding: 0}">
|
||||||
|
<gocaptcha-slide
|
||||||
|
:config="{}"
|
||||||
|
:data="comment.slideCaptchaData"
|
||||||
|
:events="commentSlideCaptchaEvent"
|
||||||
|
/>
|
||||||
|
</AModal>
|
||||||
|
</AFlex>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {h, ref} from "vue";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import imageCompression from "browser-image-compression";
|
||||||
|
import {CloseOutlined, PictureOutlined, SmileOutlined} from "@ant-design/icons-vue";
|
||||||
|
import EMOJI from "@/constant/emoji.ts";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import {useDebounceFn, useThrottleFn} from "@vueuse/core";
|
||||||
|
import {replySubmitApi} from "@/api/comment";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const comment = useStore().comment;
|
||||||
|
const user = useStore().user;
|
||||||
|
const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder'));
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
const imageList = ref<any[]>([]);
|
||||||
|
const replyContent = ref<string>("");
|
||||||
|
const topicId = ref<string>("123");
|
||||||
|
const showSubmitCaptcha = ref<boolean>(false);
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const commentSlideCaptchaEvent = {
|
||||||
|
confirm: async (point: any) => {
|
||||||
|
await replySubmitDebounced(point);
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
showSubmitCaptcha.value = false;
|
||||||
|
},
|
||||||
|
refresh: () => {
|
||||||
|
getSlideCaptchaDataThrottled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入表情到回复内容
|
||||||
|
* @param emoji
|
||||||
|
*/
|
||||||
|
async function insertEmojiToReplyContent(emoji: string) {
|
||||||
|
replyContent.value += emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 压缩图片配置
|
||||||
|
const options = {
|
||||||
|
maxSizeMB: 0.4,
|
||||||
|
maxWidthOrHeight: 750,
|
||||||
|
maxIteration: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件前置
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
async function beforeUpload(file: any) {
|
||||||
|
if (!window.FileReader) return false; // 判断是否支持FileReader
|
||||||
|
const compressedFile = await imageCompression(file, options);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(compressedFile); // 文件转换
|
||||||
|
reader.onloadend = async function () {
|
||||||
|
if (fileList.value.length >= 5) {
|
||||||
|
message.error(t('comment.maxImageCount'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fileList.value.push(reader.result);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义上传图片请求
|
||||||
|
*/
|
||||||
|
async function customUploadRequest() {
|
||||||
|
imageList.value = fileList.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除图片
|
||||||
|
* @param index
|
||||||
|
*/
|
||||||
|
async function removeBase64Image(index: number) {
|
||||||
|
fileList.value.splice(index, 1);
|
||||||
|
imageList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复提交 throttled
|
||||||
|
*/
|
||||||
|
const replySubmitDebounced = useDebounceFn(replySubmit, 500);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复提交
|
||||||
|
*/
|
||||||
|
async function replySubmit(point: any) {
|
||||||
|
if (replyContent.value.trim() === "") {
|
||||||
|
message.error(t('comment.commentContentNotEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (imageList.value.length > 3) {
|
||||||
|
message.error(t('comment.maxImageCount'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = replyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
||||||
|
|
||||||
|
const replyParams: {
|
||||||
|
images: any;
|
||||||
|
reply_id: number;
|
||||||
|
reply_user: string;
|
||||||
|
user_id: any;
|
||||||
|
author: any;
|
||||||
|
topic_id: string;
|
||||||
|
content: any;
|
||||||
|
point: [number, number];
|
||||||
|
key: any;
|
||||||
|
} = {
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
content: content,
|
||||||
|
images: imageList.value,
|
||||||
|
author: user.user.uid,
|
||||||
|
reply_id: props.item.id,
|
||||||
|
reply_user: props.item.user_id,
|
||||||
|
point: [point.x, point.y],
|
||||||
|
key: comment.slideCaptchaData.key,
|
||||||
|
};
|
||||||
|
const result: any = await replySubmitApi(replyParams);
|
||||||
|
if (result.code === 200 && result.success) {
|
||||||
|
replyContent.value = "";
|
||||||
|
fileList.value = [];
|
||||||
|
imageList.value = [];
|
||||||
|
showSubmitCaptcha.value = false;
|
||||||
|
await getReplyList();
|
||||||
|
comment.closeReplyInput();
|
||||||
|
comment.commentMap[props.item.id].reply_count++;
|
||||||
|
message.success(t('comment.replySuccess'));
|
||||||
|
} else {
|
||||||
|
message.error(t('comment.replyError'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表
|
||||||
|
*/
|
||||||
|
async function getReplyList() {
|
||||||
|
const params: any = {
|
||||||
|
topic_id: topicId.value,
|
||||||
|
page: 1,
|
||||||
|
size: 5,
|
||||||
|
comment_id: props.item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
};
|
||||||
|
await comment.getReplyList(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSlideCaptchaDataThrottled = useThrottleFn(comment.getSlideCaptchaData, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示滑动验证码
|
||||||
|
*/
|
||||||
|
async function showSlideCaptcha() {
|
||||||
|
const res = await comment.getSlideCaptchaData();
|
||||||
|
if (res) {
|
||||||
|
showSubmitCaptcha.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss" src="./index.scss">
|
||||||
|
|
||||||
|
</style>
|
52
src/components/CommentReply/Components/ReplyInput/index.scss
Normal file
52
src/components/CommentReply/Components/ReplyInput/index.scss
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.reply-input-main {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.reply-input-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-author {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-cancel {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.reply-input-content-text {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.comment-text-reply {
|
||||||
|
width: 550px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions-reply {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.comment-action-item-reply {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #767779;
|
||||||
|
|
||||||
|
.comment-action-icon-reply {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-action-icon-reply:hover {
|
||||||
|
color: #08a327;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
233
src/components/CommentReply/Components/ReplyList/ReplyList.vue
Normal file
233
src/components/CommentReply/Components/ReplyList/ReplyList.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<AFlex :vertical="true" class="reply-item-child">
|
||||||
|
<ASpin :spinning="comment.replyLoading" size="default">
|
||||||
|
<AFlex :vertical="true" v-if="comment.replyList.comments">
|
||||||
|
<AFlex :vertical="false" style="margin-top: 5px" v-for="(child, index) in comment.replyList.comments"
|
||||||
|
:key="index">
|
||||||
|
<AFlex :vertical="true" class="reply-item-child-avatar">
|
||||||
|
<AAvatar :size="40" shape="circle" :src="child.avatar"/>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="true" class="reply-item-child-content">
|
||||||
|
<AFlex :vertical="true">
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<span class="reply-name-child">{{ child.nickname }}</span>
|
||||||
|
<span
|
||||||
|
class="reply-at">@{{ child.reply_username }}</span>
|
||||||
|
<a-tag color="cyan" class="reply-tag-child" size="small">Lv.5</a-tag>
|
||||||
|
<!-- <a-tag color="red" class="reply-tag" size="small">UP</a-tag>-->
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="flex-end" justify="space-between">
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between">
|
||||||
|
<span class="reply-ip-child"> {{ child.location }} </span>
|
||||||
|
</AFlex>
|
||||||
|
<span class="reply-time-child">{{ formatTimeAgo(child.created_time) }}</span>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="true" align="center">
|
||||||
|
<ACard class="reply-card-child" :body-style="{padding: '10px'}">
|
||||||
|
<div class="reply-text-child" v-html="child.content">
|
||||||
|
</div>
|
||||||
|
<AFlex :vertical="false" align="center" class="reply-images" v-if="child.images">
|
||||||
|
<AAvatar shape="square" size="large"
|
||||||
|
v-for="(image, index) in child.images" :key="index">
|
||||||
|
<template #icon>
|
||||||
|
<AImage :width="40" :height="40" :src="image">
|
||||||
|
<template #previewMask>
|
||||||
|
<EyeOutlined style="font-size: 18px;"/>
|
||||||
|
</template>
|
||||||
|
</AImage>
|
||||||
|
</template>
|
||||||
|
</AAvatar>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" justify="space-between" align="center">
|
||||||
|
<!--评论操作按钮 -->
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between"
|
||||||
|
class="reply-action-item-child">
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AButton v-if="!child.is_liked" @click="commentLikeThrottled(child)" type="text" size="small"
|
||||||
|
:icon="h(LikeOutlined)" class="reply-action-btn-child">
|
||||||
|
{{ child.likes }}
|
||||||
|
</AButton>
|
||||||
|
<AButton v-if="child.is_liked" @click="cancelCommentLikeThrottled(child)" type="text"
|
||||||
|
size="small"
|
||||||
|
:icon="h(LikeFilled)" style="color: red" class="reply-action-btn-child">
|
||||||
|
{{ child.likes }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
<AButton
|
||||||
|
@click="comment.handleShowReplyInput(child.id)"
|
||||||
|
type="text" size="small" :icon="h(CommentOutlined)"
|
||||||
|
class="reply-action-btn-child">
|
||||||
|
{{ t('comment.reply') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 评论操作系统信息-->
|
||||||
|
<AFlex :vertical="false" align="center" justify="flex-end"
|
||||||
|
class="reply-action-item-right-child">
|
||||||
|
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)"
|
||||||
|
class="reply-action-info-child">
|
||||||
|
{{ child.operating_system }}
|
||||||
|
</AButton>
|
||||||
|
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)"
|
||||||
|
class="reply-action-info-child">
|
||||||
|
{{ child.browser }}
|
||||||
|
</AButton>
|
||||||
|
<!-- 评论操作按钮 -->
|
||||||
|
<ADropdown trigger="click">
|
||||||
|
<AButton type="text" size="small" :icon="h(EllipsisOutlined)"
|
||||||
|
class="reply-action-btn-child"
|
||||||
|
@click.prevent>
|
||||||
|
</AButton>
|
||||||
|
<template #overlay>
|
||||||
|
<AMenu>
|
||||||
|
<AMenuItem key="report">
|
||||||
|
{{ t('comment.report') }}
|
||||||
|
</AMenuItem>
|
||||||
|
<AMenuItem key="copy">
|
||||||
|
{{ t('comment.copy') }}
|
||||||
|
</AMenuItem>
|
||||||
|
<AMenuItem key="delete">
|
||||||
|
{{ t('comment.delete') }}
|
||||||
|
</AMenuItem>
|
||||||
|
</AMenu>
|
||||||
|
</template>
|
||||||
|
</ADropdown>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</ACard>
|
||||||
|
|
||||||
|
<!-- 子评论回复输入框 -->
|
||||||
|
<ReplyReply :child="child" :item="props.item"
|
||||||
|
v-if="comment.showReplyInput && comment.showReplyInput === child.id"/>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<APagination v-if="comment.replyList.total > 0" class="reply-pagination-child" size="small"
|
||||||
|
:total="comment.replyList.total"
|
||||||
|
:current="comment.replyList.current" :page-size="comment.replyList.size"
|
||||||
|
:default-page-size="comment.replyList.size"
|
||||||
|
@change="replyListThrottled"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</AFlex>
|
||||||
|
</ASpin>
|
||||||
|
</AFlex>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {h, ref} from "vue";
|
||||||
|
import {
|
||||||
|
ChromeOutlined,
|
||||||
|
CommentOutlined,
|
||||||
|
EllipsisOutlined,
|
||||||
|
LikeFilled,
|
||||||
|
LikeOutlined,
|
||||||
|
WindowsOutlined
|
||||||
|
} from "@ant-design/icons-vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import ReplyReply from "@/components/CommentReply/Components/ReplyReplyInput/ReplyReply.vue";
|
||||||
|
import {useThrottleFn} from "@vueuse/core";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
|
const comment = useStore().comment;
|
||||||
|
const user = useStore().user;
|
||||||
|
const topicId = ref<string>("123");
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间
|
||||||
|
* @param dateString
|
||||||
|
*/
|
||||||
|
function formatTimeAgo(dateString: string) {
|
||||||
|
const now: any = new Date();
|
||||||
|
const date: any = new Date(dateString);
|
||||||
|
const seconds = Math.floor((now - date) / 1000);
|
||||||
|
|
||||||
|
const intervals = [
|
||||||
|
{label: '年', seconds: 31536000},
|
||||||
|
{label: '个月', seconds: 2592000},
|
||||||
|
{label: '天', seconds: 86400},
|
||||||
|
{label: '小时', seconds: 3600},
|
||||||
|
{label: '分钟', seconds: 60}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const interval of intervals) {
|
||||||
|
const count = Math.floor(seconds / interval.seconds);
|
||||||
|
if (count > 0) {
|
||||||
|
return `${count} ${interval.label}前`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${seconds} 秒前`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentLikeThrottled = useThrottleFn(commentLike, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论点赞
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
async function commentLike(item: any) {
|
||||||
|
const params: any = {
|
||||||
|
comment_id: item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
};
|
||||||
|
const res: boolean = await comment.commentLike(params);
|
||||||
|
if (res) {
|
||||||
|
item.is_liked = true;
|
||||||
|
item.likes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelCommentLikeThrottled = useThrottleFn(cancelCommentLike, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消评论点赞
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
async function cancelCommentLike(item: any) {
|
||||||
|
const params: any = {
|
||||||
|
comment_id: item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
};
|
||||||
|
const res: boolean = await comment.cancelCommentLike(params);
|
||||||
|
if (res) {
|
||||||
|
item.is_liked = false;
|
||||||
|
item.likes--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表 throttled
|
||||||
|
*/
|
||||||
|
const replyListThrottled = useThrottleFn(getReplyList, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表
|
||||||
|
* @param page
|
||||||
|
* @param pageSize
|
||||||
|
*/
|
||||||
|
async function getReplyList(page: number, pageSize: number) {
|
||||||
|
const params: any = {
|
||||||
|
topic_id: topicId.value,
|
||||||
|
page: page,
|
||||||
|
size: pageSize,
|
||||||
|
comment_id: props.item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
};
|
||||||
|
await comment.getReplyList(params);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss" src="./index.scss">
|
||||||
|
|
||||||
|
</style>
|
115
src/components/CommentReply/Components/ReplyList/index.scss
Normal file
115
src/components/CommentReply/Components/ReplyList/index.scss
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
.reply-item-child {
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.reply-pagination-child {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-item-child-content {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.reply-at {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-at:hover {
|
||||||
|
color: #25a9e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-name-child {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-tag-child {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-left: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-ip-child {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-time-child {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-card-child {
|
||||||
|
width: 530px;
|
||||||
|
|
||||||
|
.reply-action-item-child {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.reply-action-btn-child {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-icon-child {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-icon-child:hover {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #08a327;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-btn-child:hover {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #08a327;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-icon-number-child {
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-item-right-child {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.reply-action-btn-child {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-btn-child:hover {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #08a327;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-action-info-child {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<AFlex :vertical="true" class="reply-input-main-child"
|
||||||
|
v-if="comment.showReplyInput && child.id === comment.showReplyInput">
|
||||||
|
<AFlex :vertical="false" align="center" class="reply-input-header-child">
|
||||||
|
<span class="reply-input-title-child">{{ t('comment.reply') + ':' }}</span>
|
||||||
|
<span class="reply-input-author-child">{{ props.child.nickname }}</span>
|
||||||
|
<AButton @click="comment.closeReplyInput" type="dashed" size="small"
|
||||||
|
:icon="h(CloseOutlined )"
|
||||||
|
class="reply-input-cancel-child">
|
||||||
|
{{ t('comment.cancelReply') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 回复头像-->
|
||||||
|
<AFlex :vertical="false" class="reply-input-content-child">
|
||||||
|
<AFlex :vertical="true" class="reply-input-avatar-child">
|
||||||
|
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 评论输入框 -->
|
||||||
|
<AFlex :vertical="true" class="reply-input-content-text-child">
|
||||||
|
<ATextarea :rows="3" class="comment-text-reply-child"
|
||||||
|
v-model:value="replyReplyContent"
|
||||||
|
@keyup.ctrl.enter="showSlideCaptcha"
|
||||||
|
:placeholder="commentTextAreaPlaceholder" allow-clear :showCount="false"/>
|
||||||
|
<AFlex :vertical="false" align="center" justify="space-between"
|
||||||
|
class="comment-actions-reply-child"
|
||||||
|
>
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
|
||||||
|
<APopover trigger="click" placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<div style="width: 170px;height: 200px;overflow: auto;">
|
||||||
|
<template v-for="(emoji) in EMOJI" :key="emoji">
|
||||||
|
<AButton @click="insertEmojiToReplyReplyContent(emoji)" type="text"
|
||||||
|
size="large">{{ emoji }}
|
||||||
|
</AButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<AButton type="text" size="small" :icon="h(SmileOutlined)"
|
||||||
|
class="comment-action-icon-reply-child">
|
||||||
|
{{ t('comment.emoji') }}
|
||||||
|
</AButton>
|
||||||
|
</APopover>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
|
||||||
|
<AUpload
|
||||||
|
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
|
||||||
|
name="images"
|
||||||
|
:max-count="3"
|
||||||
|
:multiple="true"
|
||||||
|
method="post"
|
||||||
|
:directory="false"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:custom-request="customUploadRequest"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:disabled="imageList.length >= 3"
|
||||||
|
>
|
||||||
|
<ABadge :count="imageList.length">
|
||||||
|
<AButton type="text" size="small" :icon="h(PictureOutlined)"
|
||||||
|
class="comment-action-icon-reply-child">
|
||||||
|
{{ t('comment.picture') }}
|
||||||
|
</AButton>
|
||||||
|
</ABadge>
|
||||||
|
</AUpload>
|
||||||
|
<template v-if="imageList.length > 0">
|
||||||
|
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList"
|
||||||
|
:key="index">
|
||||||
|
<template #count>
|
||||||
|
<CloseCircleOutlined @click="removeBase64Image(index)"
|
||||||
|
style="color: #f5222d"/>
|
||||||
|
</template>
|
||||||
|
<AAvatar shape="square" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<AImage v-if="item" :width="24" :height="24" :src="item"/>
|
||||||
|
</template>
|
||||||
|
</AAvatar>
|
||||||
|
</ABadge>
|
||||||
|
</template>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center">
|
||||||
|
<AButton
|
||||||
|
@click="showSlideCaptcha"
|
||||||
|
:disabled="replyReplyContent.trim().length === 0" type="primary" size="middle"
|
||||||
|
class="comment-action-btn-reply-child">
|
||||||
|
{{ t('comment.sendComment') }}
|
||||||
|
</AButton>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
</AFlex>
|
||||||
|
<AModal v-model:open="showSubmitCaptcha" :footer="null" :closable="false" width="375" :centered="true"
|
||||||
|
:maskClosable="false" :bodyStyle="{padding: 0}">
|
||||||
|
<gocaptcha-slide
|
||||||
|
:config="{}"
|
||||||
|
:data="comment.slideCaptchaData"
|
||||||
|
:events="commentSlideCaptchaEvent"
|
||||||
|
/>
|
||||||
|
</AModal>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {h, ref} from "vue";
|
||||||
|
import {CloseOutlined, PictureOutlined, SmileOutlined} from "@ant-design/icons-vue";
|
||||||
|
import EMOJI from "@/constant/emoji.ts";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import {replyReplySubmitApi} from "@/api/comment";
|
||||||
|
import {ReplyCommentParams} from "@/types/comment";
|
||||||
|
import {useThrottleFn} from "@vueuse/core";
|
||||||
|
import imageCompression from "browser-image-compression";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder'));
|
||||||
|
const comment = useStore().comment;
|
||||||
|
const replyReplyContent = ref<string>("");
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
const imageList = ref<any[]>([]);
|
||||||
|
const user = useStore().user;
|
||||||
|
const topicId = ref<string>("123");
|
||||||
|
const showSubmitCaptcha = ref<boolean>(false);
|
||||||
|
const props = defineProps({
|
||||||
|
child: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
const commentSlideCaptchaEvent = {
|
||||||
|
confirm: async (point: any) => {
|
||||||
|
await replyReplySubmitThrottled(point);
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
showSubmitCaptcha.value = false;
|
||||||
|
},
|
||||||
|
refresh: () => {
|
||||||
|
getSlideCaptchaDataThrottled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入表情到回复内容
|
||||||
|
* @param emoji
|
||||||
|
*/
|
||||||
|
async function insertEmojiToReplyReplyContent(emoji: string) {
|
||||||
|
replyReplyContent.value += emoji;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 压缩图片配置
|
||||||
|
const options = {
|
||||||
|
maxSizeMB: 0.4,
|
||||||
|
maxWidthOrHeight: 750,
|
||||||
|
maxIteration: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件前置
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
async function beforeUpload(file: any) {
|
||||||
|
if (!window.FileReader) return false; // 判断是否支持FileReader
|
||||||
|
const compressedFile = await imageCompression(file, options);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(compressedFile); // 文件转换
|
||||||
|
reader.onloadend = async function () {
|
||||||
|
if (fileList.value.length >= 5) {
|
||||||
|
message.error(t('comment.maxImageCount'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fileList.value.push(reader.result);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义上传图片请求
|
||||||
|
*/
|
||||||
|
async function customUploadRequest() {
|
||||||
|
imageList.value = fileList.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除图片
|
||||||
|
* @param index
|
||||||
|
*/
|
||||||
|
async function removeBase64Image(index: number) {
|
||||||
|
fileList.value.splice(index, 1);
|
||||||
|
imageList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复提交 throttled
|
||||||
|
*/
|
||||||
|
const replyReplySubmitThrottled = useThrottleFn(replyReplySubmit, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复评论提交
|
||||||
|
*/
|
||||||
|
async function replyReplySubmit(point: any) {
|
||||||
|
if (replyReplyContent.value.trim() === "") {
|
||||||
|
message.error(t('comment.commentContentNotEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (imageList.value.length > 3) {
|
||||||
|
message.error(t('comment.maxImageCount'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = replyReplyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
|
||||||
|
|
||||||
|
const replyParams: ReplyCommentParams = {
|
||||||
|
user_id: user.user.uid,
|
||||||
|
topic_id: topicId.value,
|
||||||
|
content: content,
|
||||||
|
images: imageList.value,
|
||||||
|
author: user.user.uid,
|
||||||
|
reply_to: props.child.id,
|
||||||
|
reply_id: props.item.id,
|
||||||
|
reply_user: props.child.user_id,
|
||||||
|
point: [point.x, point.y],
|
||||||
|
key: comment.slideCaptchaData.key,
|
||||||
|
};
|
||||||
|
const result: any = await replyReplySubmitApi(replyParams);
|
||||||
|
if (result.code === 200 && result.success) {
|
||||||
|
|
||||||
|
replyReplyContent.value = "";
|
||||||
|
fileList.value = [];
|
||||||
|
imageList.value = [];
|
||||||
|
showSubmitCaptcha.value = false;
|
||||||
|
await getReplyList();
|
||||||
|
comment.closeReplyInput();
|
||||||
|
comment.commentMap[props.item.id].reply_count++;
|
||||||
|
message.success(t('comment.replySuccess'));
|
||||||
|
} else {
|
||||||
|
message.error(t('comment.replyError'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSlideCaptchaDataThrottled = useThrottleFn(comment.getSlideCaptchaData, 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示滑动验证码
|
||||||
|
*/
|
||||||
|
async function showSlideCaptcha() {
|
||||||
|
const res = await comment.getSlideCaptchaData();
|
||||||
|
if (res) {
|
||||||
|
showSubmitCaptcha.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表
|
||||||
|
*/
|
||||||
|
async function getReplyList() {
|
||||||
|
const params: any = {
|
||||||
|
topic_id: topicId.value,
|
||||||
|
page: 1,
|
||||||
|
size: 5,
|
||||||
|
comment_id: props.item.id,
|
||||||
|
user_id: user.user.uid,
|
||||||
|
};
|
||||||
|
await comment.getReplyList(params);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss" src="./index.scss">
|
||||||
|
|
||||||
|
</style>
|
@@ -0,0 +1,52 @@
|
|||||||
|
.reply-input-main-child {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.reply-input-title-child {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-author-child {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-cancel-child {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #767779;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-content-child {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.reply-input-content-text-child {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.comment-text-reply-child {
|
||||||
|
width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions-reply-child {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.comment-action-item-reply-child {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #767779;
|
||||||
|
|
||||||
|
.comment-action-icon-reply-child {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #767779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-action-icon-reply-child:hover {
|
||||||
|
color: #08a327;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -13,376 +13,8 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-content {
|
|
||||||
margin-left: 20px;
|
|
||||||
|
|
||||||
.comment-text {
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.comment-action-item {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #767779;
|
|
||||||
|
|
||||||
|
|
||||||
.comment-action-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-action-icon:hover {
|
|
||||||
color: #08a327;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-header {
|
|
||||||
.reply-header-title {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-header-count {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-header-sort {
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-list {
|
|
||||||
margin-top: 30px;
|
|
||||||
|
|
||||||
.reply-pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-content {
|
|
||||||
margin-left: 20px;
|
|
||||||
|
|
||||||
.reply-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-tag {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-left: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-ip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-card {
|
|
||||||
width: 600px;
|
|
||||||
//margin-top: 5px;
|
|
||||||
|
|
||||||
.reply-images {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-item {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.reply-action-btn {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-icon:hover {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #08a327;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-btn:hover {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #08a327;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-icon-number {
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-item-right {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.reply-action-btn {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-btn:hover {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #08a327;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-info {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-main {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.reply-input-title {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-author {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-cancel {
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-content {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.reply-input-content-text {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.comment-text-reply {
|
|
||||||
width: 550px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions-reply {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.comment-action-item-reply {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #767779;
|
|
||||||
|
|
||||||
.comment-action-icon-reply {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-action-icon-reply:hover {
|
|
||||||
color: #08a327;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-item-child {
|
|
||||||
margin-top: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.reply-pagination-child {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-item-child-content {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.reply-at {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-at:hover {
|
|
||||||
color: #25a9e3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-name-child {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-tag-child {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-left: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-ip-child {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-time-child {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-card-child {
|
|
||||||
width: 530px;
|
|
||||||
|
|
||||||
.reply-action-item-child {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.reply-action-btn-child {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-icon-child {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-icon-child:hover {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #08a327;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-btn-child:hover {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #08a327;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-icon-number-child {
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-item-right-child {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.reply-action-btn-child {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-btn-child:hover {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #08a327;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-action-info-child {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-main-child {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.reply-input-title-child {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-author-child {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-cancel-child {
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #767779;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-input-content-child {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
.reply-input-content-text-child {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.comment-text-reply-child {
|
|
||||||
width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions-reply-child {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.comment-action-item-reply-child {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #767779;
|
|
||||||
|
|
||||||
.comment-action-icon-reply-child {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #767779;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-action-icon-reply-child:hover {
|
|
||||||
color: #08a327;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
22
src/components/CommentReply/index.vue
Normal file
22
src/components/CommentReply/index.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment-main">
|
||||||
|
<AFlex :vertical="false" justify="flex-start" class="comment-header">
|
||||||
|
<span class="comment-header-title">{{ t('comment.comment') }}</span>
|
||||||
|
</AFlex>
|
||||||
|
<!-- 评论输入框 -->
|
||||||
|
<CommentInput/>
|
||||||
|
<ADivider/>
|
||||||
|
<!-- 回复列表 -->
|
||||||
|
<CommentList/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CommentInput from "@/components/CommentReply/Components/CommentInput/CommentInput.vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import CommentList from "@/components/CommentReply/Components/CommentList/CommentList.vue";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
</script>
|
||||||
|
<style src="./index.scss" lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
@@ -94,5 +94,11 @@ export default {
|
|||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
cancelReply: 'Cancel Reply',
|
cancelReply: 'Cancel Reply',
|
||||||
|
commentContentNotEmpty: 'Comment content cannot be empty!',
|
||||||
|
maxImageCount: 'Maximum image count reached!',
|
||||||
|
commentSuccess: 'comment success!',
|
||||||
|
commentError: 'comment failed!',
|
||||||
|
replySuccess: 'reply success!',
|
||||||
|
replyError: 'reply failed!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -93,6 +93,12 @@ export default {
|
|||||||
delete: '删除',
|
delete: '删除',
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
cancelReply: '取消',
|
cancelReply: '取消',
|
||||||
|
commentContentNotEmpty: '评论内容不能为空!',
|
||||||
|
maxImageCount: '最多只能上传3张图片!',
|
||||||
|
commentSuccess: '评论成功!',
|
||||||
|
commentError: '评论失败!',
|
||||||
|
replySuccess: '回复成功!',
|
||||||
|
replyError: '回复失败!',
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@@ -2,6 +2,7 @@ import {useAuthStore} from '@/store/modules/userStore.ts';
|
|||||||
import {useThemeStore} from "@/store/modules/themeStore.ts";
|
import {useThemeStore} from "@/store/modules/themeStore.ts";
|
||||||
import {langStore} from "@/store/modules/langStore.ts";
|
import {langStore} from "@/store/modules/langStore.ts";
|
||||||
import {useClientStore} from "@/store/modules/clientStore.ts";
|
import {useClientStore} from "@/store/modules/clientStore.ts";
|
||||||
|
import {useCommentStore} from "@/store/modules/commentStore.ts";
|
||||||
|
|
||||||
export default function useStore() {
|
export default function useStore() {
|
||||||
return {
|
return {
|
||||||
@@ -9,5 +10,6 @@ export default function useStore() {
|
|||||||
theme: useThemeStore(),
|
theme: useThemeStore(),
|
||||||
lang: langStore(),
|
lang: langStore(),
|
||||||
client: useClientStore(),
|
client: useClientStore(),
|
||||||
|
comment: useCommentStore(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
173
src/store/modules/commentStore.ts
Normal file
173
src/store/modules/commentStore.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {reactive, ref} from "vue";
|
||||||
|
import {Comment} from "@/types/comment";
|
||||||
|
import {cancelCommentLikeApi, commentLikeApi, commentListApi, replyListApi} from "@/api/comment";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import {getSlideCaptchaDataApi} from "@/api/captcha";
|
||||||
|
|
||||||
|
|
||||||
|
export const useCommentStore = defineStore(
|
||||||
|
'comment',
|
||||||
|
() => {
|
||||||
|
const commentList = ref<Comment>({} as Comment);
|
||||||
|
const commentLoading = ref<boolean>(true);
|
||||||
|
const replyLoading = ref<boolean>(true);
|
||||||
|
const showReplyInput = ref<number | null>(null);
|
||||||
|
const showCommentReply = ref<number | null>(null);
|
||||||
|
const replyList = ref<Comment>({} as Comment);
|
||||||
|
const commentMap = reactive<any>({});
|
||||||
|
const slideCaptchaData = reactive({
|
||||||
|
key: "",
|
||||||
|
image: "",
|
||||||
|
thumb: "",
|
||||||
|
thumbWidth: 0,
|
||||||
|
thumbHeight: 0,
|
||||||
|
thumbX: 0,
|
||||||
|
thumbY: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论列表
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
async function getCommentList(params: any) {
|
||||||
|
const data: any = {
|
||||||
|
user_id: params.user_id,
|
||||||
|
topic_id: params.topic_id,
|
||||||
|
page: params.page,
|
||||||
|
size: params.size,
|
||||||
|
is_hot: params.is_hot,
|
||||||
|
};
|
||||||
|
commentLoading.value = true;
|
||||||
|
commentList.value = {} as Comment;
|
||||||
|
// 获取评论列表
|
||||||
|
const result: any = await commentListApi(data);
|
||||||
|
if (result.code === 200 && result.success && result.data) {
|
||||||
|
commentList.value = result.data;
|
||||||
|
commentLoading.value = false;
|
||||||
|
commentList.value.comments.forEach((item: any) => {
|
||||||
|
commentMap[item.id] = item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示回复输入框
|
||||||
|
*/
|
||||||
|
const handleShowReplyInput = (index: any) => {
|
||||||
|
showReplyInput.value = showReplyInput.value === index ? null : index;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 关闭回复输入框
|
||||||
|
*/
|
||||||
|
const closeReplyInput = () => {
|
||||||
|
showReplyInput.value = null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 是否显示回复
|
||||||
|
*/
|
||||||
|
const handleShowCommentReply = (index: any) => {
|
||||||
|
showCommentReply.value = showCommentReply.value === index ? null : index;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回复列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async function getReplyList(data: any) {
|
||||||
|
const params: any = {
|
||||||
|
topic_id: data.topic_id,
|
||||||
|
page: data.page,
|
||||||
|
size: data.size,
|
||||||
|
comment_id: data.comment_id,
|
||||||
|
user_id: data.user_id,
|
||||||
|
};
|
||||||
|
replyLoading.value = true;
|
||||||
|
replyList.value = {} as Comment;
|
||||||
|
// 获取评论列表
|
||||||
|
const result: any = await replyListApi(params);
|
||||||
|
if (result.code === 200 && result.success && result.data) {
|
||||||
|
replyList.value = result.data;
|
||||||
|
replyLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论点赞
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async function commentLike(data: any): Promise<boolean> {
|
||||||
|
const params: any = {
|
||||||
|
comment_id: data.comment_id,
|
||||||
|
user_id: data.user_id,
|
||||||
|
topic_id: data.topic_id,
|
||||||
|
};
|
||||||
|
const result: any = await commentLikeApi(params);
|
||||||
|
if (result.code !== 200 || !result.success) {
|
||||||
|
message.error(result.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消评论点赞
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async function cancelCommentLike(data: any): Promise<boolean> {
|
||||||
|
const params: any = {
|
||||||
|
comment_id: data.comment_id,
|
||||||
|
user_id: data.user_id,
|
||||||
|
topic_id: data.topic_id,
|
||||||
|
};
|
||||||
|
const result: any = await cancelCommentLikeApi(params);
|
||||||
|
if (result.code !== 200 || !result.success) {
|
||||||
|
message.error(result.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取滑动验证码数据
|
||||||
|
*/
|
||||||
|
async function getSlideCaptchaData(): Promise<boolean> {
|
||||||
|
const res: any = await getSlideCaptchaDataApi();
|
||||||
|
if (res.code == 200 && res.data) {
|
||||||
|
const {key, image, thumb, thumb_width, thumb_height, thumb_x, thumb_y} = res.data;
|
||||||
|
slideCaptchaData.key = key;
|
||||||
|
slideCaptchaData.image = image;
|
||||||
|
slideCaptchaData.thumb = thumb;
|
||||||
|
slideCaptchaData.thumbWidth = thumb_width;
|
||||||
|
slideCaptchaData.thumbHeight = thumb_height;
|
||||||
|
slideCaptchaData.thumbX = thumb_x;
|
||||||
|
slideCaptchaData.thumbY = thumb_y;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commentList,
|
||||||
|
commentLoading,
|
||||||
|
showReplyInput,
|
||||||
|
showCommentReply,
|
||||||
|
replyList,
|
||||||
|
replyLoading,
|
||||||
|
slideCaptchaData,
|
||||||
|
commentMap,
|
||||||
|
getCommentList,
|
||||||
|
handleShowReplyInput,
|
||||||
|
closeReplyInput,
|
||||||
|
handleShowCommentReply,
|
||||||
|
getReplyList,
|
||||||
|
commentLike,
|
||||||
|
cancelCommentLike,
|
||||||
|
getSlideCaptchaData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 开启数据持久化
|
||||||
|
persist: false,
|
||||||
|
}
|
||||||
|
);
|
4
src/types/comment.d.ts
vendored
4
src/types/comment.d.ts
vendored
@@ -10,7 +10,6 @@ interface CommentContent {
|
|||||||
browser: string;
|
browser: string;
|
||||||
content: string;
|
content: string;
|
||||||
created_time: string;
|
created_time: string;
|
||||||
dislikes: number;
|
|
||||||
id: number;
|
id: number;
|
||||||
likes: number;
|
likes: number;
|
||||||
location: string;
|
location: string;
|
||||||
@@ -25,6 +24,7 @@ interface CommentContent {
|
|||||||
nickname: string;
|
nickname: string;
|
||||||
level?: number;
|
level?: number;
|
||||||
images: string[];
|
images: string[];
|
||||||
|
is_liked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplyCommentParams {
|
export interface ReplyCommentParams {
|
||||||
@@ -36,4 +36,6 @@ export interface ReplyCommentParams {
|
|||||||
reply_id: number,
|
reply_id: number,
|
||||||
reply_user: string,
|
reply_user: string,
|
||||||
reply_to: number,
|
reply_to: number,
|
||||||
|
point: [number, number]
|
||||||
|
key: string
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||||
<h1>Welcome to Main Page</h1>
|
|
||||||
<AButton @click="handleClick">获取登录用户角色</AButton>
|
<AButton @click="handleClick">获取登录用户角色</AButton>
|
||||||
{{ data }}
|
{{ data }}
|
||||||
|
|
||||||
<CommentReply/>
|
<CommentReply/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
import {useRequest} from "alova/client";
|
import {useRequest} from "alova/client";
|
||||||
import {getUserPermissions} from "@/api/user";
|
import {getUserPermissions} from "@/api/user";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import CommentReply from "@/components/CommentReply/CommentReply.vue";
|
import CommentReply from "@/components/CommentReply/index.vue";
|
||||||
|
|
||||||
|
|
||||||
const {data, send} = useRequest(getUserPermissions, {
|
const {data, send} = useRequest(getUserPermissions, {
|
||||||
immediate: false
|
immediate: false
|
||||||
@@ -20,6 +21,8 @@ const handleClick = () => {
|
|||||||
const userId: string = userInfo.user.uid;
|
const userId: string = userInfo.user.uid;
|
||||||
send(userId);
|
send(userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user