Commit 9bf17898 by lijiabin

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

parent 49ec66d5
......@@ -24,6 +24,10 @@
<div class="flex items-center gap-3">
<el-skeleton-item
variant="text"
style="width: 52px; height: 28px; border-radius: 6px"
/>
<el-skeleton-item
variant="text"
style="width: 72px; height: 28px; border-radius: 6px"
/>
<el-skeleton-item
......@@ -134,7 +138,20 @@
编辑
</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
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"
......@@ -237,6 +254,7 @@
:initial-index="currentPreviewIndex"
@close="showPreview = false"
/>
<SendMessageDialog ref="sendMessageDialogRef" />
</div>
</template>
......@@ -245,6 +263,7 @@ import dayjs from 'dayjs'
import type { ArticleItemDto } from '@/api'
import { articleTypeListOptions, ArticleTypeEnum, VideoPositionEnum } from '@/constants'
import ActionMore from '@/components/common/ActionMore/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import type { Directive } from 'vue'
......@@ -254,13 +273,22 @@ const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const router = useRouter()
const { jumpToUserHomePage } = useNavigation()
const { articleDetail } = defineProps<{
const { articleDetail, isAudit } = defineProps<{
articleDetail: ArticleItemDto
isAudit: boolean // 是否是审核页面
}>()
const sendMessageDialogRef = ref<InstanceType<typeof SendMessageDialog> | null>(null)
const articleType = computed(() => {
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)
......
<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 @@
:src="userInfo?.showAvatar"
class="border-4 border-white shadow-lg"
/>
<div class="flex-1">
<h2 class="text-xl font-semibold text-gray-800 mb-1 mt-4">
{{ userInfo?.showName }}
</h2>
<p v-if="!+(route.params.isReal as string)" class="text-gray-500 text-sm mb-2">
{{ userInfo?.signature }}
</p>
<!-- <el-button type="warning" size="small" plain @click="handleEdit">
<el-icon>
<IEpEdit />
</el-icon>
修改资料
</el-button> -->
<div
class="flex-1 min-w-0 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
>
<div class="min-w-0">
<h2 class="text-xl font-semibold text-gray-800 mb-1 mt-4 sm:mt-1">
{{ userInfo?.showName }}
</h2>
<p v-if="!+(route.params.isReal as string)" class="text-gray-500 text-sm mb-0">
{{ userInfo?.signature }}
</p>
</div>
<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>
......@@ -69,9 +76,9 @@
</div>
<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 || '--' }}
</el-link>
</span>
</div>
<!-- <el-divider /> -->
</div>
......@@ -90,6 +97,7 @@
</div>
</div>
</div>
<SendMessageDialog ref="sendMessageDialogRef" />
</div>
</template>
......@@ -98,11 +106,37 @@ import { hasOfficialAccount, getOtherUserData, getOtherUserPostData } from '@/ap
import type { OfficialAccountItemDto } from '@/api/user/types'
import dayjs from 'dayjs'
import { usePageSearch, useNavigation } from '@/hooks'
import { articleTypeListOptions } from '@/constants'
import { articleTypeListOptions, BooleanFlag } from '@/constants'
import type { OtherUserInfoDto } from '@/api/otherUserPage/types'
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 { 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(
getOtherUserPostData,
{
......
......@@ -116,7 +116,7 @@
>
{{ questionDetail.createUserName }}
</el-avatar>
<div class="flex flex-col">
<div class="flex flex-col flex-1 min-w-0">
<span class="text-sm text-slate-900">
{{ questionDetail.createUserName }}
</span>
......@@ -132,6 +132,19 @@
{{ questionDetail.viewCount }}阅读
</span>
</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>
<!-- 标题:主要信息,黑重粗 -->
<h1 class="text-2xl font-bold text-slate-900 mb-3 leading-snug tracking-tight">
......@@ -405,6 +418,7 @@
</div>
</div>
<CommentDialog ref="commentDialogRef" @commentSuccess="refresh" />
<SendMessageDialog ref="sendMessageDialogRef" />
</div>
</template>
<script setup lang="ts">
......@@ -420,6 +434,7 @@ import type { ArticleItemDto, CommentItemDto } from '@/api'
import { usePageSearch } from '@/hooks'
import Comment from '@/components/common/Comment/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 dayjs from 'dayjs'
import { useUserStore } from '@/stores/user'
......@@ -439,6 +454,9 @@ const commentRefList = ref<InstanceType<typeof Comment>[]>([])
const answerRefList = ref<HTMLElement[]>([])
const questionDetail = ref<ArticleItemDto>({} as ArticleItemDto)
const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef')
const sendMessageDialogRef = useTemplateRef<InstanceType<typeof SendMessageDialog> | null>(
'sendMessageDialogRef',
)
// 回滚到子评论框
const { handleBackTop: handleBackTopChildren } = useScrollTop(answerRefList)
......
......@@ -201,6 +201,12 @@ const { userInfo } = storeToRefs(userStore)
// 左侧普通用户菜单
const menuUserItems = [
{
path: '/selfMessage',
label: '我的私信',
icon: () => <IEpMessage />,
tab: '我的私信',
},
{
path: '/userPage/selfPublish',
label: '我的帖子',
......@@ -254,13 +260,7 @@ const menuUserItems = [
label: '回答问题(问吧)',
icon: () => <IEpChatLineSquare />,
tab: '回答问题',
},
{
path: '/selfMessage',
label: '我的私信',
icon: () => <IEpMessage />,
tab: '我的私信',
},
}
]
// 左侧官方账号菜单
......
......@@ -125,10 +125,23 @@
{{ videoDetail?.createUserName }}
</h3>
</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 class="flex items-center">
<div class="flex items-center flex-wrap gap-2 sm:gap-0">
<!-- 浏览量 -->
<el-button
text
......@@ -312,6 +325,7 @@
<RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" />
<RewardToast ref="rewardToastRef" :rewardVideoType="rewardVideoType" />
<RewardFullSetToast ref="rewardFullSetToastRef" @hided="videoRef?.play()" />
<SendMessageDialog ref="sendMessageDialogRef" />
</div>
</template>
<script lang="ts" setup>
......@@ -330,10 +344,12 @@ import RewardDialog from './components/rewardDialog.vue'
import RewardToast from './components/rewardToast.vue'
import RewardFullSetToast from './components/rewardFullSetToast.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 { useNavigation } from '@/hooks'
import {
ArticleTypeEnum,
BooleanFlag,
SpecificVideoRewardEnum,
specificVideoRewardListOptions,
} from '@/constants'
......@@ -378,6 +394,11 @@ const watchedSecondsObj = useStorage(`watched-seconds-obj-${userInfo.value.userI
})
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 specificVideoRewardItem = computed(() => {
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