updated comment

This commit is contained in:
landaiqing
2024-09-24 01:14:50 +08:00
parent 74dc2b36c8
commit a5624466ae
8 changed files with 668 additions and 314 deletions

14
components.d.ts vendored
View File

@@ -9,7 +9,6 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AAvatar: typeof import('ant-design-vue/es')['Avatar'] AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
@@ -20,17 +19,16 @@ declare module 'vue' {
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image'] AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AMenu: typeof import('ant-design-vue/es')['Menu'] AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
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'] ASegmented: typeof import('ant-design-vue/es')['Segmented']
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']
ATabs: typeof import('ant-design-vue/es')['Tabs'] ATabs: typeof import('ant-design-vue/es')['Tabs']
@@ -40,40 +38,30 @@ declare module 'vue' {
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']
ChromeOutlined: typeof import('@ant-design/icons-vue')['ChromeOutlined']
ClockCircleOutlined: typeof import('@ant-design/icons-vue')['ClockCircleOutlined']
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'] CommentReply: typeof import('./src/components/CommentReply/CommentReply.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']
DislikeOutlined: typeof import('@ant-design/icons-vue')['DislikeOutlined']
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
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']
EllipsisOutlined: typeof import('@ant-design/icons-vue')['EllipsisOutlined']
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']
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default'] LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
LikeOutlined: typeof import('@ant-design/icons-vue')['LikeOutlined']
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined'] LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default'] LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default'] LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default']
MainPage: typeof import('./src/views/Main/MainPage.vue')['default'] MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
NotFound: typeof import('./src/views/404/NotFound.vue')['default'] NotFound: typeof import('./src/views/404/NotFound.vue')['default']
PictureOutlined: typeof import('@ant-design/icons-vue')['PictureOutlined']
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
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']
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']
SmileOutlined: typeof import('@ant-design/icons-vue')['SmileOutlined']
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined'] TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined'] UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined'] WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined'] WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
WindowsOutlined: typeof import('@ant-design/icons-vue')['WindowsOutlined']
} }
} }

View File

@@ -13,6 +13,7 @@ export const commentSubmitApi = (params: any) => {
author: params.author, author: params.author,
}, },
{ {
name: 'comment-submit',
meta: { meta: {
ignoreToken: false, ignoreToken: false,
}, },
@@ -34,6 +35,7 @@ export const replySubmitApi = (params: any) => {
author: params.author, author: params.author,
}, },
{ {
name: 'reply-submit',
meta: { meta: {
ignoreToken: false, ignoreToken: false,
}, },
@@ -52,7 +54,59 @@ export const commentListApi = (params: any) => {
topic_id: params.topic_id, topic_id: params.topic_id,
}, },
{ {
cacheFor: 1000 * 60 * 60 * 24 * 7, // 7天缓存 cacheFor: {
expire: 60 * 60 * 24 * 7,
mode: "restore",
}, // 7天缓存
hitSource: "comment-submit",
meta: {
ignoreToken: false,
},
}
);
};
/**
* @description 评论列表
* @param params
*/
export const replyListApi = (params: any) => {
return service.Post('/api/auth/reply/list', {
page: params.page,
size: params.size,
comment_id: params.comment_id,
topic_id: params.topic_id,
},
{
cacheFor: {
expire: 60 * 60 * 24 * 7,
mode: "restore",
}, // 7天缓存
hitSource: ["reply-submit", "reply-reply-submit"],
meta: {
ignoreToken: false,
},
}
);
};
/**
* @description 回复的回复提交
* @param params
*/
export const replyReplySubmitApi = (params: any) => {
return service.Post('/api/auth/reply/reply/submit', {
user_id: params.user_id,
content: params.content,
images: params.images,
topic_id: params.topic_id,
reply_to_user: params.reply_to_user,
reply_to: params.reply_to,
reply_id: params.reply_id,
reply_user: params.reply_user,
author: params.author,
},
{
name: 'reply-reply-submit',
meta: { meta: {
ignoreToken: false, ignoreToken: false,
}, },

View File

@@ -89,315 +89,432 @@
<ASegmented v-model:value="segmentedValue" :options="data" class="reply-header-sort"/> <ASegmented v-model:value="segmentedValue" :options="data" class="reply-header-sort"/>
</AFlex> </AFlex>
</div> </div>
<div class="reply-list"> <ASkeleton :loading="commentLoading" avatar active :paragraph="{ rows: 4 }"
<div class="reply-item"> >
<AFlex :vertical="false"> <div class="reply-list" v-if="commentList?.comments">
<!-- 评论头像 --> <div class="reply-item" v-for="(item, index) in commentList?.comments" :key="index">
<AFlex :vertical="true" class="reply-avatar"> <AFlex :vertical="false" style="margin-top: 5px">
<AAvatar :size="50" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/> <!-- 评论头像 -->
</AFlex> <AFlex :vertical="true" class="reply-avatar" v-if="item.avatar">
<!-- 评论内容 --> <AAvatar :size="50" shape="circle" :src="item.avatar"/>
<AFlex :vertical="true" class="reply-content">
<AFlex :vertical="true">
<AFlex :vertical="false" align="flex-start">
<span class="reply-name">张立国</span>
<a-tag color="cyan" class="reply-tag" 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"> 成都市 </span>
<span class="reply-ip" style="margin-left: 10px;">IP: 192.168.1.100 </span>
</AFlex>
<span class="reply-time">{{ new Date().toLocaleString() }}</span>
</AFlex>
</AFlex> </AFlex>
<AFlex :vertical="false" align="center"> <!-- 评论内容 -->
<AFlex :vertical="true" class="reply-content">
<ACard class="reply-card" :body-style="{padding: '10px'}"> <AFlex :vertical="true">
<span class="reply-text"> <AFlex :vertical="false" align="flex-start">
床前明月光疑是地上霜<br> <span class="reply-name">{{ item.nickname }}</span>
举头望明月低头思故乡 <a-tag color="cyan" class="reply-tag" size="small">Lv.5</a-tag>
</span> <a-tag color="red" class="reply-tag" size="small" v-if="item.author===1">UP</a-tag>
<AFlex :vertical="false" justify="space-between" align="center"> </AFlex>
<!--评论操作按钮 --> <AFlex :vertical="false" align="flex-end" justify="space-between">
<AFlex :vertical="false" align="center" justify="space-between" class="reply-action-item"> <AFlex :vertical="false" align="center" justify="space-between">
<AFlex :vertical="false" align="center"> <span class="reply-ip"> {{ item.location }} </span>
<AButton type="text" size="small" :icon="h(LikeOutlined)" class="reply-action-btn">
10
</AButton>
</AFlex>
<AFlex :vertical="false" align="center">
<AButton type="text" size="small" :icon="h(DislikeOutlined)" class="reply-action-btn">
1
</AButton>
</AFlex>
<AButton type="text" size="small" :icon="h(MessageOutlined)" class="reply-action-btn">
11
</AButton>
<AButton
@click="replyInputVisible === true? (replyInputVisible = false) : (replyInputVisible = true) "
type="text" size="small" :icon="h(CommentOutlined)"
class="reply-action-btn">
{{ t('comment.reply') }}
</AButton>
</AFlex> </AFlex>
<!-- 评论操作系统信息--> <span class="reply-time">{{ formatTimeAgo(item.created_time) }}</span>
<AFlex :vertical="false" align="center" justify="flex-end" class="reply-action-item-right"> </AFlex>
<AButton type="text" disabled size="small" :icon="h(WindowsOutlined)" class="reply-action-info"> </AFlex>
windows 10 <AFlex :vertical="false" align="center">
</AButton>
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)" class="reply-action-info"> <ACard class="reply-card" :body-style="{padding: '10px'}">
chrome <div class="reply-text" v-html="item.content">
</AButton> </div>
<!-- 评论操作按钮 --> <AFlex :vertical="false" align="center" class="reply-images" v-if="item.images">
<ADropdown trigger="click"> <AAvatar shape="square" size="large"
<AButton type="text" size="small" :icon="h(EllipsisOutlined)" class="reply-action-btn" v-for="(image, index) in item.images" :key="index">
@click.prevent> <template #icon>
</AButton> <AImage :width="40" :height="40" :src="image"/>
<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> </template>
</ADropdown> </AAvatar>
</AFlex> </AFlex>
</AFlex> <AFlex :vertical="false" justify="space-between" align="center">
</ACard> <!--评论操作按钮 -->
<AFlex :vertical="false" align="center" justify="space-between" class="reply-action-item">
</AFlex> <AFlex :vertical="false" align="center">
<!-- 回复输入框 --> <AButton type="text" size="small" :icon="h(LikeOutlined)" class="reply-action-btn">
<AFlex :vertical="true" class="reply-input-main" v-show="replyInputVisible"> {{ item.likes }}
<AFlex :vertical="false" align="center" class="reply-input-header">
<span class="reply-input-title">{{ t('comment.reply') + '' }}</span>
<span class="reply-input-author">张立国</span>
<AButton @click="replyInputVisible = false" 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"
: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">
<AButton type="text" size="small" :icon="h(SmileOutlined)"
class="comment-action-icon-reply">
{{ t('comment.emoji') }}
</AButton> </AButton>
</AFlex> </AFlex>
<AFlex :vertical="false" align="center" class="comment-action-item-reply"> <AFlex :vertical="false" align="center">
<AUpload <AButton type="text" size="small" :icon="h(DislikeOutlined)" class="reply-action-btn">
:accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'" {{ item.dislikes }}
name="images" </AButton>
:max-count="3" </AFlex>
:multiple="true" <AButton @click="replyListThrottled(item.id)" type="text" size="small"
method="post" :icon="h(MessageOutlined)"
:directory="false" class="reply-action-btn">
:show-upload-list="false" {{ item.reply_count }}
:custom-request="customUploadRequest" </AButton>
:before-upload="beforeUpload" <AButton
:disabled="imageList.length >= 3" @click="handleShowReplyInput(item.id)"
> type="text" size="small" :icon="h(CommentOutlined)"
<ABadge :count="imageList.length"> class="reply-action-btn">
<AButton type="text" size="small" :icon="h(PictureOutlined)" {{ t('comment.reply') }}
class="comment-action-icon-reply"> </AButton>
{{ t('comment.picture') }} </AFlex>
</AButton> <!-- 评论操作系统信息-->
</ABadge> <AFlex :vertical="false" align="center" justify="flex-end" class="reply-action-item-right">
</AUpload> <AButton type="text" disabled size="small" :icon="h(WindowsOutlined)" class="reply-action-info">
<template v-if="imageList.length > 0"> {{ item.operating_system }}
<ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index"> </AButton>
<template #count> <AButton type="text" disabled size="small" :icon="h(ChromeOutlined)" class="reply-action-info">
<CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/> {{ item.browser }}
</template> </AButton>
<AAvatar shape="square" size="small"> <!-- 评论操作按钮 -->
<template #icon> <ADropdown trigger="click">
<AImage v-if="item" :width="24" :height="24" :src="item"/> <AButton type="text" size="small" :icon="h(EllipsisOutlined)" class="reply-action-btn"
</template> @click.prevent>
</AAvatar> </AButton>
</ABadge> <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> </template>
</AFlex> </ADropdown>
</AFlex>
<AFlex :vertical="false" align="center">
<AButton @click="replySubmitThrottled" type="primary" size="middle"
:disabled="replyContent.trim().length === 0"
class="comment-action-btn-reply">
{{ t('comment.sendComment') }}
</AButton>
</AFlex> </AFlex>
</AFlex> </AFlex>
</AFlex> </ACard>
</AFlex>
</AFlex>
<!-- 子回复列表 -->
<AFlex :vertical="false" class="reply-item-child">
<AFlex :vertical="true" class="reply-item-child-avatar">
<AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
</AFlex> </AFlex>
<AFlex :vertical="true" class="reply-item-child-content"> <!-- 回复输入框 -->
<AFlex :vertical="true"> <AFlex :vertical="true" class="reply-input-main" v-if="showReplyInput && item.id === showReplyInput">
<AFlex :vertical="false" align="center"> <AFlex :vertical="false" align="center" class="reply-input-header">
<span class="reply-name-child">沈建明</span> <span class="reply-at">@张立国</span> <span class="reply-input-title">{{ t('comment.reply') + '' }}</span>
<a-tag color="cyan" class="reply-tag-child" size="small">Lv.5</a-tag> <span class="reply-input-author">{{ item.nickname }}</span>
<!-- <a-tag color="red" class="reply-tag" size="small">UP</a-tag>--> <AButton @click="closeReplyInput" type="dashed" size="small" :icon="h(CloseOutlined )"
</AFlex> class="reply-input-cancel">
<AFlex :vertical="false" align="flex-end" justify="space-between"> {{ t('comment.cancelReply') }}
<AFlex :vertical="false" align="center" justify="space-between"> </AButton>
<span class="reply-ip-child"> 成都市 </span>
<span class="reply-ip-child" style="margin-left: 10px;">IP: 192.168.1.100 </span>
</AFlex>
<span class="reply-time-child">{{ new Date().toLocaleString() }}</span>
</AFlex>
</AFlex> </AFlex>
<AFlex :vertical="true" align="center"> <!-- 回复头像-->
<ACard class="reply-card-child" :body-style="{padding: '10px'}"> <AFlex :vertical="false" class="reply-input-content">
<span class="reply-text-child"> <AFlex :vertical="true" class="reply-input-avatar">
床前明月光疑是地上霜<br> <AAvatar :size="40" shape="circle" src="https://api.multiavatar.com/landaiqing.svg"/>
举头望明月低头思故乡 </AFlex>
</span> <!-- 评论输入框 -->
<AFlex :vertical="false" justify="space-between" align="center"> <AFlex :vertical="true" class="reply-input-content-text">
<!--评论操作按钮 --> <ATextarea :rows="3" class="comment-text-reply"
<AFlex :vertical="false" align="center" justify="space-between" v-model:value="replyContent"
class="reply-action-item-child"> @keyup.ctrl.enter="()=>{
<AFlex :vertical="false" align="center"> const params: any ={
<AButton type="text" size="small" :icon="h(LikeOutlined)" class="reply-action-btn-child"> reply_id: item.id,
10 reply_user: item.user_id
</AButton> }
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>
<AFlex :vertical="false" align="center"> <AFlex :vertical="false" align="center" class="comment-action-item-reply">
<AButton type="text" size="small" :icon="h(DislikeOutlined)" <AUpload
class="reply-action-btn-child"> :accept="'image/jpg, image/png, image/jpeg, image/gif, image/svg+xml, image/webp'"
1 name="images"
</AButton> :max-count="3"
</AFlex> :multiple="true"
<AButton type="text" size="small" :icon="h(MessageOutlined)" class="reply-action-btn-child"> method="post"
11 :directory="false"
</AButton> :show-upload-list="false"
<AButton :custom-request="customUploadRequest"
@click="replyInputVisible === true? (replyInputVisible = false) : (replyInputVisible = true) " :before-upload="beforeUpload"
type="text" size="small" :icon="h(CommentOutlined)" :disabled="imageList.length >= 3"
class="reply-action-btn-child"> >
{{ t('comment.reply') }} <ABadge :count="imageList.length">
</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">
windows 10
</AButton>
<AButton type="text" disabled size="small" :icon="h(ChromeOutlined)"
class="reply-action-info-child">
chrome
</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-show="replyInputVisible">
<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">沈建明</span>
<AButton @click="replyInputVisible = false" 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"
: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">
<AButton type="text" size="small" :icon="h(SmileOutlined)"
class="comment-action-icon-reply-child">
{{ t('comment.emoji') }}
</AButton>
</AFlex>
<AFlex :vertical="false" align="center" class="comment-action-item-reply-child">
<AButton type="text" size="small" :icon="h(PictureOutlined)" <AButton type="text" size="small" :icon="h(PictureOutlined)"
class="comment-action-icon-reply-child"> class="comment-action-icon-reply">
{{ t('comment.picture') }} {{ t('comment.picture') }}
</AButton> </AButton>
</AFlex> </ABadge>
</AFlex> </AUpload>
<AFlex :vertical="false" align="center"> <template v-if="imageList.length > 0">
<AButton type="primary" size="middle" class="comment-action-btn-reply-child"> <ABadge style="margin-left: 10px;" v-for="(item, index) in imageList" :key="index">
{{ t('comment.sendComment') }} <template #count>
</AButton> <CloseCircleOutlined @click="removeBase64Image(index)" style="color: #f5222d"/>
</AFlex> </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> </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> </AFlex>
</AFlex> </AFlex>
</AFlex>
</AFlex>
</AFlex> <!-- 子回复列表 -->
<AFlex :vertical="true" class="reply-item-child"
v-if="replyList.comments && showReplyComment && showReplyComment === item.id">
<ASpin :spinning="replyLoading">
<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>
</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>
</ASpin>
</AFlex>
</AFlex>
</AFlex>
</div>
<APagination class="reply-pagination" @change="(page, pageSize)=>{
console.log(page, pageSize)
}" v-model:current="commentList.current" :size="commentList.size.toString()" :total="commentList.total"
show-less-items/>
</div> </div>
</div> </ASkeleton>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {h, reactive, ref} from "vue"; import {h, onMounted, reactive, ref} from "vue";
import { import {
ChromeOutlined, ChromeOutlined,
CloseOutlined, CloseOutlined,
@@ -414,20 +531,27 @@ import EMOJI from "@/constant/emoji.ts";
import imageCompression from "browser-image-compression"; import imageCompression from "browser-image-compression";
import {message} from "ant-design-vue"; import {message} from "ant-design-vue";
import {useThrottleFn} from "@vueuse/core"; import {useThrottleFn} from "@vueuse/core";
import {commentListApi, commentSubmitApi, replySubmitApi} from "@/api/comment"; import {commentListApi, commentSubmitApi, replyListApi, replyReplySubmitApi, replySubmitApi} from "@/api/comment";
import useStore from "@/store"; import useStore from "@/store";
import {Comment, ReplyCommentParams} from "@/types/comment";
const {t} = useI18n(); const {t} = useI18n();
const showCommentActions = ref<boolean>(false); const showCommentActions = ref<boolean>(false);
const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder')); const commentTextAreaPlaceholder = ref<string>(t('comment.placeholder'));
const data = reactive([t('comment.latest'), t('comment.hot')]); const data = reactive([t('comment.latest'), t('comment.hot')]);
const segmentedValue = ref<string>(data[0]); const segmentedValue = ref<string>(data[0]);
const replyInputVisible = ref<boolean>(false);
const commentContent = ref<string>(""); const commentContent = ref<string>("");
const replyContent = ref<string>(""); const replyContent = ref<string>("");
const replyReplyContent = ref<string>("");
const fileList = ref<any[]>([]); const fileList = ref<any[]>([]);
const imageList = ref<any[]>([]); const imageList = ref<any[]>([]);
const user = useStore().user; 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);
/** /**
* 聚焦事件 * 聚焦事件
@@ -444,6 +568,22 @@ async function insertEmoji(emoji: string) {
commentContent.value += emoji; 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 = { const options = {
maxSizeMB: 0.4, maxSizeMB: 0.4,
@@ -518,6 +658,7 @@ async function commentSubmit() {
commentContent.value = ""; commentContent.value = "";
fileList.value = []; fileList.value = [];
imageList.value = []; imageList.value = [];
await getCommentList();
} else { } else {
message.error("评论失败"); message.error("评论失败");
} }
@@ -531,8 +672,8 @@ const replySubmitThrottled = useThrottleFn(replySubmit, 1000);
/** /**
* 回复提交 * 回复提交
*/ */
async function replySubmit() { async function replySubmit(data: ReplyCommentParams) {
if (commentContent.value.trim() === "") { if (replyContent.value.trim() === "") {
message.error("回复内容不能为空"); message.error("回复内容不能为空");
return; return;
} }
@@ -540,21 +681,21 @@ async function replySubmit() {
message.error("最多只能上传3张图片"); message.error("最多只能上传3张图片");
return; return;
} }
const content = commentContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '); const content = replyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
const replyParams: object = { const replyParams: ReplyCommentParams = {
user_id: user.user.uid, user_id: user.user.uid,
topic_id: "123", topic_id: "123",
content: content, content: content,
images: imageList.value, images: imageList.value,
author: user.user.uid, author: user.user.uid,
reply_id: "5", reply_id: data.reply_id,
reply_user: user.user.uid, reply_user: data.reply_user,
}; };
const result: any = await replySubmitApi(replyParams); const result: any = await replySubmitApi(replyParams);
if (result.code === 200 && result.success) { if (result.code === 200 && result.success) {
message.success("回复成功"); message.success("回复成功");
commentContent.value = ""; replyContent.value = "";
fileList.value = []; fileList.value = [];
imageList.value = []; imageList.value = [];
} else { } else {
@@ -569,14 +710,132 @@ async function getCommentList() {
const params = { const params = {
topic_id: "123", topic_id: "123",
page: 1, page: 1,
size: 10, size: 5,
}; };
// 获取评论列表 // 获取评论列表
const result: any = await commentListApi(params); const result: any = await commentListApi(params);
console.log(result); if (result.code === 200 && result.success && result.data) {
commentList.value = result.data;
commentLoading.value = false;
}
} }
getCommentList(); /**
* 格式化时间
* @param dateString
*/
function formatTimeAgo(dateString) {
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
*/
async function getReplyList(reply_id: number) {
const params: any = {
topic_id: "123",
page: 1,
size: 5,
comment_id: reply_id,
};
// 获取评论列表
const result: any = await replyListApi(params);
if (result.code === 200 && result.success && result.data) {
replyList.value = result.data;
handleShowReplyComment(reply_id);
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: "123",
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> </script>
<style src="./index.scss" lang="scss" scoped> <style src="./index.scss" lang="scss" scoped>

View File

@@ -3,7 +3,7 @@
flex-direction: column; flex-direction: column;
border: 1px solid #ccc; border: 1px solid #ccc;
margin-top: 20px; margin-top: 20px;
width: 100%; width: 650px;
padding: 50px; padding: 50px;
.comment-header-title { .comment-header-title {
@@ -19,6 +19,7 @@
.comment-text { .comment-text {
width: 600px; width: 600px;
} }
.comment-actions { .comment-actions {
margin-top: 10px; margin-top: 10px;
@@ -61,6 +62,12 @@
.reply-list { .reply-list {
margin-top: 30px; margin-top: 30px;
.reply-pagination {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.reply-content { .reply-content {
margin-left: 20px; margin-left: 20px;
@@ -91,9 +98,12 @@
width: 600px; width: 600px;
//margin-top: 5px; //margin-top: 5px;
.reply-images {
margin-top: 5px;
}
.reply-action-item { .reply-action-item {
margin-top: 10px; margin-top: 10px;
width: 200px;
.reply-action-btn { .reply-action-btn {
font-size: 13px; font-size: 13px;
@@ -253,7 +263,6 @@
.reply-action-item-child { .reply-action-item-child {
margin-top: 10px; margin-top: 10px;
width: 200px;
.reply-action-btn-child { .reply-action-btn-child {
font-size: 13px; font-size: 13px;

View File

@@ -1 +1,39 @@
export interface Comment {
comments: CommentContent[];
current: number;
total: number;
size: number;
}
interface CommentContent {
author: number;
browser: string;
content: string;
created_time: string;
dislikes: number;
id: number;
likes: number;
location: string;
operating_system: string;
reply_count: number;
reply_username: string;
reply_id: string;
reply_user: string;
topic_id: string;
user_id: string;
avatar: string;
nickname: string;
level?: number;
images: string[];
}
interface ReplyCommentParams {
user_id: string,
topic_id: string,
content: string,
images: string[],
author: string,
reply_id: number,
reply_user: string,
reply_to: number,
}

View File

@@ -55,6 +55,7 @@ export const service = createAlova({
if (!method.meta?.ignoreToken) { if (!method.meta?.ignoreToken) {
const user = useStore().user; const user = useStore().user;
method.config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${user.user.accessToken}`; method.config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${user.user.accessToken}`;
method.config.headers['X-UID'] = user.user.uid;
} }
const lang = useStore().lang; const lang = useStore().lang;
method.config.headers['Accept-Language'] = lang.lang || 'zh'; method.config.headers['Accept-Language'] = lang.lang || 'zh';

View File

@@ -54,7 +54,7 @@
</AButton> </AButton>
<AButton v-if="state.showCountDown" disabled style="margin-left: 10px" size="large"> <AButton v-if="state.showCountDown" disabled style="margin-left: 10px" size="large">
{{ {{
state.countDownTime currentCountDownTime
}}s{{ t("login.reSendCaptcha") }} }}s{{ t("login.reSendCaptcha") }}
</AButton> </AButton>
</AFlex> </AFlex>
@@ -187,7 +187,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {Rule} from "ant-design-vue/lib/form"; import {Rule} from "ant-design-vue/lib/form";
import {onMounted, reactive, ref, UnwrapRef} from "vue"; import {computed, onMounted, reactive, ref, UnwrapRef} from "vue";
import {AccountLogin, PhoneLogin} from "@/types/user"; import {AccountLogin, PhoneLogin} from "@/types/user";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import BoxDog from "@/components/BoxDog/BoxDog.vue"; import BoxDog from "@/components/BoxDog/BoxDog.vue";
@@ -272,11 +272,16 @@ const rules: Record<string, Rule[]> = {
] ]
}; };
const state: any = reactive<any>({ interface State {
countDownTime: number;
showCountDown: boolean;
}
const state = reactive<State>({
countDownTime: 60, countDownTime: 60,
showCountDown: false, showCountDown: false,
}); } as State);
const currentCountDownTime = computed(() => state.countDownTime);
/** /**
* 验证码发送倒计时 * 验证码发送倒计时
*/ */

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
<h1>Welcome to Main Page</h1> <h1>Welcome to Main Page</h1>
<AButton @click="handleClick">获取登录用户角色</AButton> <AButton @click="handleClick">获取登录用户角色</AButton>
{{ data }} {{ data }}