Commit 9bf17898 by lijiabin

【需求 21402】 feat: 加入发送私信的弹窗,并在帖子详情页完成发送私信功能

parent 49ec66d5
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<el-skeleton-item <el-skeleton-item
variant="text" variant="text"
style="width: 52px; height: 28px; border-radius: 6px"
/>
<el-skeleton-item
variant="text"
style="width: 72px; height: 28px; border-radius: 6px" style="width: 72px; height: 28px; border-radius: 6px"
/> />
<el-skeleton-item <el-skeleton-item
...@@ -134,7 +138,20 @@ ...@@ -134,7 +138,20 @@
编辑 编辑
</el-link> </el-link>
<!-- 优化后的右侧内容 --> <!-- 优化后的右侧内容 -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3 flex-wrap justify-end">
<button
v-if="!isAudit && !isAuthor && articleDetail.createUserId"
type="button"
class="inline-flex items-center px-3.5 py-1.5 text-sm font-medium bg-blue-50 text-blue-600 rounded-lg border border-blue-100/80 shadow-sm hover:bg-blue-100 hover:border-blue-200/80 active:scale-[0.98] transition-all cursor-pointer shrink-0"
@click="
sendMessageDialogRef?.open({
receiverId: articleDetail.createUserId,
isReal: isReal,
})
"
>
私信
</button>
<span <span
class="px-3 py-1.5 text-sm font-medium bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors" class="px-3 py-1.5 text-sm font-medium bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors"
v-if="articleDetail.relateColumn" v-if="articleDetail.relateColumn"
...@@ -237,6 +254,7 @@ ...@@ -237,6 +254,7 @@
:initial-index="currentPreviewIndex" :initial-index="currentPreviewIndex"
@close="showPreview = false" @close="showPreview = false"
/> />
<SendMessageDialog ref="sendMessageDialogRef" />
</div> </div>
</template> </template>
...@@ -245,6 +263,7 @@ import dayjs from 'dayjs' ...@@ -245,6 +263,7 @@ import dayjs from 'dayjs'
import type { ArticleItemDto } from '@/api' import type { ArticleItemDto } from '@/api'
import { articleTypeListOptions, ArticleTypeEnum, VideoPositionEnum } from '@/constants' import { articleTypeListOptions, ArticleTypeEnum, VideoPositionEnum } from '@/constants'
import ActionMore from '@/components/common/ActionMore/index.vue' import ActionMore from '@/components/common/ActionMore/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import type { Directive } from 'vue' import type { Directive } from 'vue'
...@@ -254,13 +273,22 @@ const userStore = useUserStore() ...@@ -254,13 +273,22 @@ const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const router = useRouter() const router = useRouter()
const { jumpToUserHomePage } = useNavigation() const { jumpToUserHomePage } = useNavigation()
const { articleDetail } = defineProps<{ const { articleDetail, isAudit } = defineProps<{
articleDetail: ArticleItemDto articleDetail: ArticleItemDto
isAudit: boolean // 是否是审核页面 isAudit: boolean // 是否是审核页面
}>() }>()
const sendMessageDialogRef = ref<InstanceType<typeof SendMessageDialog> | null>(null)
const articleType = computed(() => { const articleType = computed(() => {
return articleTypeListOptions.find((item) => item.value === articleDetail.type)?.label return articleTypeListOptions.find((item) => item.value === articleDetail.type)?.label
}) })
const isReal = computed(() => {
return +(
articleDetail.type === ArticleTypeEnum.PRACTICE ||
articleDetail.type === ArticleTypeEnum.INTERVIEW ||
articleDetail.type === ArticleTypeEnum.QUESTION
)
})
const loading = computed(() => !articleDetail.title) const loading = computed(() => !articleDetail.title)
......
<template>
<el-dialog
v-model="visible"
:title="form.chatType === BooleanFlag.YES ? '发送匿名私信' : '发送实名私信'"
width="500px"
:before-close="handleClose"
top="30vh"
class="send-message-dialog"
destroy-on-close
>
<div class="flex gap-3">
<el-avatar :size="40" :src="avatar" />
<CommentBox
class="flex-1"
:textAreaHeight="100"
placeholder="输入私信内容…"
v-model:inputText="form.content"
v-model:inputImg="form.images"
>
<template #submit>
<el-button
:disabled="isDisabled"
:loading="loading"
type="primary"
round
@click="handleSubmit"
class="px-5 !rounded-lg text-sm font-medium shadow-sm"
>发送</el-button
>
</template>
</CommentBox>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { sendMessage } from '@/api'
import CommentBox from '../CommentBox/index.vue'
import { push } from 'notivue'
import { BooleanFlag } from '@/constants'
import type { SendMessageDto } from '@/api'
import { useResetData } from '@/hooks'
const emit = defineEmits<{
(e: 'sendSuccess'): void
}>()
const visible = ref(false)
const loading = ref(false)
const isDisabled = computed(() => {
return !form.value.content || loading.value
})
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const [form] = useResetData<SendMessageDto>({
content: '',
chatType: BooleanFlag.NO,
senderId: userInfo.value.userId,
receiverId: '',
images: '',
})
const avatar = computed(() => {
return form.value.chatType === BooleanFlag.YES
? userInfo.value.hiddenAvatar
: userInfo.value.avatar
})
const open = ({ receiverId, isReal }: { receiverId: string; isReal: BooleanFlag }) => {
visible.value = true
form.value.receiverId = receiverId
form.value.chatType = isReal ? BooleanFlag.NO : BooleanFlag.YES
}
const handleClose = () => {
visible.value = false
form.value.content = ''
form.value.images = ''
}
const handleSubmit = async () => {
loading.value = true
try {
await sendMessage(form.value)
push.success('发送成功')
handleClose()
emit('sendSuccess')
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
defineExpose({
open,
})
</script>
<style scoped>
.send-message-dialog :deep(.el-dialog__header) {
margin-right: 0;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
</style>
...@@ -19,19 +19,26 @@ ...@@ -19,19 +19,26 @@
:src="userInfo?.showAvatar" :src="userInfo?.showAvatar"
class="border-4 border-white shadow-lg" class="border-4 border-white shadow-lg"
/> />
<div class="flex-1"> <div
<h2 class="text-xl font-semibold text-gray-800 mb-1 mt-4"> class="flex-1 min-w-0 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
{{ userInfo?.showName }} >
</h2> <div class="min-w-0">
<p v-if="!+(route.params.isReal as string)" class="text-gray-500 text-sm mb-2"> <h2 class="text-xl font-semibold text-gray-800 mb-1 mt-4 sm:mt-1">
{{ userInfo?.signature }} {{ userInfo?.showName }}
</p> </h2>
<!-- <el-button type="warning" size="small" plain @click="handleEdit"> <p v-if="!+(route.params.isReal as string)" class="text-gray-500 text-sm mb-0">
<el-icon> {{ userInfo?.signature }}
<IEpEdit /> </p>
</el-icon> </div>
修改资料 <button
</el-button> --> v-if="canSendPrivateMessage"
type="button"
class="shrink-0 self-start sm:self-auto inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50/90 border border-blue-100 rounded-lg hover:bg-blue-100/80 hover:border-blue-200/80 active:scale-[0.98] transition-colors cursor-pointer"
@click="openSendMessage"
>
<el-icon class="text-[15px] text-blue-600"><IEpChatDotRound /></el-icon>
私信
</button>
</div> </div>
</div> </div>
</div> </div>
...@@ -69,9 +76,9 @@ ...@@ -69,9 +76,9 @@
</div> </div>
<div class="flex items-center text-gray-400 text-sm ml-4"> <div class="flex items-center text-gray-400 text-sm ml-4">
<el-link type="primary" link> <span class="text-blue-600">
{{ articleTypeListOptions.find((i) => i.value === item.type)?.label || '--' }} {{ articleTypeListOptions.find((i) => i.value === item.type)?.label || '--' }}
</el-link> </span>
</div> </div>
<!-- <el-divider /> --> <!-- <el-divider /> -->
</div> </div>
...@@ -90,6 +97,7 @@ ...@@ -90,6 +97,7 @@
</div> </div>
</div> </div>
</div> </div>
<SendMessageDialog ref="sendMessageDialogRef" />
</div> </div>
</template> </template>
...@@ -98,11 +106,37 @@ import { hasOfficialAccount, getOtherUserData, getOtherUserPostData } from '@/ap ...@@ -98,11 +106,37 @@ import { hasOfficialAccount, getOtherUserData, getOtherUserPostData } from '@/ap
import type { OfficialAccountItemDto } from '@/api/user/types' import type { OfficialAccountItemDto } from '@/api/user/types'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { usePageSearch, useNavigation } from '@/hooks' import { usePageSearch, useNavigation } from '@/hooks'
import { articleTypeListOptions } from '@/constants' import { articleTypeListOptions, BooleanFlag } from '@/constants'
import type { OtherUserInfoDto } from '@/api/otherUserPage/types' import type { OtherUserInfoDto } from '@/api/otherUserPage/types'
import BackButton from '@/components/common/BackButton/index.vue' import BackButton from '@/components/common/BackButton/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
const route = useRoute() const route = useRoute()
const { jumpToArticleDetailPage } = useNavigation() const { jumpToArticleDetailPage } = useNavigation()
const userStore = useUserStore()
const { userInfo: loginUser } = storeToRefs(userStore)
const sendMessageDialogRef = ref<InstanceType<typeof SendMessageDialog> | null>(null)
const targetUserId = computed(() => (route.params.userId as string) || '')
const routeIsRealFlag = computed(() =>
+(route.params.isReal as string) === 1 ? BooleanFlag.YES : BooleanFlag.NO,
)
const canSendPrivateMessage = computed(() => {
const id = targetUserId.value
return !!id && id !== loginUser.value.userId
})
const openSendMessage = () => {
sendMessageDialogRef.value?.open({
receiverId: targetUserId.value,
isReal: routeIsRealFlag.value,
})
}
const { list, total, searchParams, goToPage, changePageSize } = usePageSearch( const { list, total, searchParams, goToPage, changePageSize } = usePageSearch(
getOtherUserPostData, getOtherUserPostData,
{ {
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
> >
{{ questionDetail.createUserName }} {{ questionDetail.createUserName }}
</el-avatar> </el-avatar>
<div class="flex flex-col"> <div class="flex flex-col flex-1 min-w-0">
<span class="text-sm text-slate-900"> <span class="text-sm text-slate-900">
{{ questionDetail.createUserName }} {{ questionDetail.createUserName }}
</span> </span>
...@@ -132,6 +132,19 @@ ...@@ -132,6 +132,19 @@
{{ questionDetail.viewCount }}阅读 {{ questionDetail.viewCount }}阅读
</span> </span>
</div> </div>
<button
v-if="!isAuthor && questionDetail.createUserId"
type="button"
class="shrink-0 inline-flex items-center px-3 py-1.5 text-xs font-semibold text-blue-600 bg-blue-50 rounded-lg border border-blue-100 hover:bg-blue-100 hover:border-blue-200/80 active:scale-[0.98] transition-all cursor-pointer"
@click="
sendMessageDialogRef?.open({
receiverId: String(questionDetail.createUserId),
isReal: BooleanFlag.YES,
})
"
>
私信
</button>
</div> </div>
<!-- 标题:主要信息,黑重粗 --> <!-- 标题:主要信息,黑重粗 -->
<h1 class="text-2xl font-bold text-slate-900 mb-3 leading-snug tracking-tight"> <h1 class="text-2xl font-bold text-slate-900 mb-3 leading-snug tracking-tight">
...@@ -405,6 +418,7 @@ ...@@ -405,6 +418,7 @@
</div> </div>
</div> </div>
<CommentDialog ref="commentDialogRef" @commentSuccess="refresh" /> <CommentDialog ref="commentDialogRef" @commentSuccess="refresh" />
<SendMessageDialog ref="sendMessageDialogRef" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -420,6 +434,7 @@ import type { ArticleItemDto, CommentItemDto } from '@/api' ...@@ -420,6 +434,7 @@ import type { ArticleItemDto, CommentItemDto } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import Comment from '@/components/common/Comment/index.vue' import Comment from '@/components/common/Comment/index.vue'
import CommentDialog from '@/components/common/CommentDialog/index.vue' import CommentDialog from '@/components/common/CommentDialog/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
import BackButton from '@/components/common/BackButton/index.vue' import BackButton from '@/components/common/BackButton/index.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
...@@ -439,6 +454,9 @@ const commentRefList = ref<InstanceType<typeof Comment>[]>([]) ...@@ -439,6 +454,9 @@ const commentRefList = ref<InstanceType<typeof Comment>[]>([])
const answerRefList = ref<HTMLElement[]>([]) const answerRefList = ref<HTMLElement[]>([])
const questionDetail = ref<ArticleItemDto>({} as ArticleItemDto) const questionDetail = ref<ArticleItemDto>({} as ArticleItemDto)
const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef') const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef')
const sendMessageDialogRef = useTemplateRef<InstanceType<typeof SendMessageDialog> | null>(
'sendMessageDialogRef',
)
// 回滚到子评论框 // 回滚到子评论框
const { handleBackTop: handleBackTopChildren } = useScrollTop(answerRefList) const { handleBackTop: handleBackTopChildren } = useScrollTop(answerRefList)
......
...@@ -201,6 +201,12 @@ const { userInfo } = storeToRefs(userStore) ...@@ -201,6 +201,12 @@ const { userInfo } = storeToRefs(userStore)
// 左侧普通用户菜单 // 左侧普通用户菜单
const menuUserItems = [ const menuUserItems = [
{
path: '/selfMessage',
label: '我的私信',
icon: () => <IEpMessage />,
tab: '我的私信',
},
{ {
path: '/userPage/selfPublish', path: '/userPage/selfPublish',
label: '我的帖子', label: '我的帖子',
...@@ -254,13 +260,7 @@ const menuUserItems = [ ...@@ -254,13 +260,7 @@ const menuUserItems = [
label: '回答问题(问吧)', label: '回答问题(问吧)',
icon: () => <IEpChatLineSquare />, icon: () => <IEpChatLineSquare />,
tab: '回答问题', tab: '回答问题',
}, }
{
path: '/selfMessage',
label: '我的私信',
icon: () => <IEpMessage />,
tab: '我的私信',
},
] ]
// 左侧官方账号菜单 // 左侧官方账号菜单
......
...@@ -125,10 +125,23 @@ ...@@ -125,10 +125,23 @@
{{ videoDetail?.createUserName }} {{ videoDetail?.createUserName }}
</h3> </h3>
</div> </div>
<button
v-if="!isVideoAuthor && videoDetail?.createUserId"
type="button"
class="mr-1 sm:mr-2 inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-600 bg-blue-50 rounded-lg border border-blue-100/90 shadow-sm hover:bg-blue-100 hover:border-blue-200/80 active:scale-[0.98] transition-all cursor-pointer shrink-0"
@click="
sendMessageDialogRef?.open({
receiverId: String(videoDetail.createUserId),
isReal: BooleanFlag.NO,
})
"
>
私信
</button>
</div> </div>
<!-- 右侧:互动按钮 --> <!-- 右侧:互动按钮 -->
<div class="flex items-center"> <div class="flex items-center flex-wrap gap-2 sm:gap-0">
<!-- 浏览量 --> <!-- 浏览量 -->
<el-button <el-button
text text
...@@ -312,6 +325,7 @@ ...@@ -312,6 +325,7 @@
<RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" /> <RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" />
<RewardToast ref="rewardToastRef" :rewardVideoType="rewardVideoType" /> <RewardToast ref="rewardToastRef" :rewardVideoType="rewardVideoType" />
<RewardFullSetToast ref="rewardFullSetToastRef" @hided="videoRef?.play()" /> <RewardFullSetToast ref="rewardFullSetToastRef" @hided="videoRef?.play()" />
<SendMessageDialog ref="sendMessageDialogRef" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
...@@ -330,10 +344,12 @@ import RewardDialog from './components/rewardDialog.vue' ...@@ -330,10 +344,12 @@ import RewardDialog from './components/rewardDialog.vue'
import RewardToast from './components/rewardToast.vue' import RewardToast from './components/rewardToast.vue'
import RewardFullSetToast from './components/rewardFullSetToast.vue' import RewardFullSetToast from './components/rewardFullSetToast.vue'
import ActionMore from '@/components/common/ActionMore/index.vue' import ActionMore from '@/components/common/ActionMore/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
import BackButton from '@/components/common/BackButton/index.vue' import BackButton from '@/components/common/BackButton/index.vue'
import { useNavigation } from '@/hooks' import { useNavigation } from '@/hooks'
import { import {
ArticleTypeEnum, ArticleTypeEnum,
BooleanFlag,
SpecificVideoRewardEnum, SpecificVideoRewardEnum,
specificVideoRewardListOptions, specificVideoRewardListOptions,
} from '@/constants' } from '@/constants'
...@@ -378,6 +394,11 @@ const watchedSecondsObj = useStorage(`watched-seconds-obj-${userInfo.value.userI ...@@ -378,6 +394,11 @@ const watchedSecondsObj = useStorage(`watched-seconds-obj-${userInfo.value.userI
}) })
const rewardDialogRef = useTemplateRef<InstanceType<typeof RewardDialog> | null>('rewardDialogRef') const rewardDialogRef = useTemplateRef<InstanceType<typeof RewardDialog> | null>('rewardDialogRef')
const sendMessageDialogRef = useTemplateRef<InstanceType<typeof SendMessageDialog> | null>(
'sendMessageDialogRef',
)
const isVideoAuthor = computed(() => videoDetail.value.createUserId === userInfo.value.userId)
const rewardVideoLimitDuration = ref(0) const rewardVideoLimitDuration = ref(0)
const specificVideoRewardItem = computed(() => { const specificVideoRewardItem = computed(() => {
return specificVideoRewardListOptions.find((item) => return specificVideoRewardListOptions.find((item) =>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment