2025-09-16 16:19:29 +08:00

497 lines
12 KiB
Vue

<template>
<view
:class="[
'conversation-item-container',
{
'show-action-list': showMoreActions,
'stick-on-top': conversation.stickTop,
},
]"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@click="handleConversationItemClick()"
>
<view class="conversation-item-content">
<view class="conversation-item-left">
<!-- 会话Item未读数 -->
<view class="unread" v-if="unread">
<view class="dot" v-if="isMute"></view>
<view class="badge" v-else>{{ unread }}</view>
</view>
<!-- 会话头像 -->
<Avatar :account="to" :avatar="teamAvatar" />
<!-- 用户在线离线状态 -->
<view
class="login-state-icon"
v-if="
loginStateVisible &&
!isAiUser &&
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
status
"
></view>
<view
class="unlogin-state-icon"
v-if="
loginStateVisible &&
!isAiUser &&
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
!status
"
></view>
</view>
<view class="conversation-item-right">
<!-- 会话名称 -->
<view class="conversation-item-top">
<Appellation
class="conversation-item-title"
v-if="
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
"
:account="to"
/>
<span v-else class="conversation-item-title">
{{ sessionName }}
</span>
<span class="conversation-item-time">{{ date }}</span>
</view>
<view class="conversation-item-desc">
<!-- 是否有人@ -->
<span v-if="beMentioned" class="beMentioned">
{{ '[' + t('someoneText') + '@' + t('meText') + ']' }}
</span>
<!-- 会话最后一条消息是否已读 -->
<!-- <ConversationItemIsRead
v-if="showConversationUnread"
:conversation="props.conversation"
></ConversationItemIsRead> -->
<!-- 会话最后一条消息外露 -->
<span
v-if="props.conversation.lastMessage"
class="conversation-item-desc-content"
>
<LastMsgContent :lastMessage="props.conversation.lastMessage" />
</span>
<!-- 消息免打扰 -->
<span class="conversation-item-desc-ait">
<Icon
v-if="isMute"
iconClassName="conversation-item-desc-state"
type="icon-xiaoximiandarao"
color="#ccc"
/>
</span>
</view>
</view>
</view>
<!-- 消息右键操作列表 -->
<view class="right-action-list">
<view
v-for="action in moreActions"
:key="action.type"
:class="['right-action-item', action.class]"
@click="() => handleClick(action.type)"
>
{{ action.name }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
/** 会话列表Item组件 */
import Avatar from '@/components/Avatar.vue'
import Appellation from '@/components/Appellation.vue'
import Icon from '@/components/Icon.vue'
import { computed, onUnmounted, withDefaults } from 'vue'
import dayjs from 'dayjs'
import { t } from '@/utils/im/i18n'
import { V2NIMConst } from '@/utils/im/nim'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
import ConversationItemIsRead from './conversation-item-isRead.vue'
import LastMsgContent from './conversation-item-last-msg-content.vue'
import { ref } from 'vue'
import { autorun } from 'mobx'
const props = withDefaults(
defineProps<{
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
showMoreActions?: boolean
}>(),
{ showMoreActions: false }
)
const emit = defineEmits(['click', 'delete', 'stickyToTop', 'leftSlide'])
/** 右滑操作列表 */
const moreActions = computed(() => {
return [
{
name: props.conversation.stickTop
? t('deleteStickTopText')
: t('addStickTopText'),
class: 'action-top',
type: 'action-top',
},
{
name: t('deleteSessionText'),
class: 'action-delete',
type: 'action-delete',
},
]
})
/** 对话方 */
const to = computed(() => {
const res = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
props.conversation.conversationId
)
return res
})
/** 全局配置的是否需要展示在线离线状态 */
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
/** 当前会话方在线离线状态 */
const status = ref<boolean>(false)
/** 右滑操作点击 */
const handleClick = (type: string) => {
if (type === 'action-top') {
emit('stickyToTop', props.conversation)
} else {
emit('delete', props.conversation)
}
}
/** 群头像 */
const teamAvatar = computed(() => {
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
const { avatar } = props.conversation
return avatar
}
})
/** 会话昵称 */
const sessionName = computed(() => {
if (props.conversation.name) {
return props.conversation.name
}
return props.conversation.conversationId
})
/** 是否是机器人 */
const isAiUser = ref(false)
/** 时间 */
const date = computed(() => {
const time =
props.conversation.lastMessage?.messageRefer.createTime ||
props.conversation.updateTime
// 如果最后一条消息时间戳不存在,则会话列表不显示
if (!time) {
return ''
}
const _d = dayjs(time)
const isCurrentDay = _d.isSame(dayjs(), 'day')
const isCurrentYear = _d.isSame(dayjs(), 'year')
return _d.format(
isCurrentDay ? 'HH:mm' : isCurrentYear ? 'MM-DD HH:mm' : 'YYYY-MM-DD HH:mm'
)
})
const max = 99
/** 未读数 */
const unread = computed(() => {
return props.conversation.unreadCount > 0
? props.conversation.unreadCount > max
? `${max}+`
: props.conversation.unreadCount + ''
: ''
})
/** 是否免打扰 */
const isMute = computed(() => {
return !!props.conversation.mute
})
/** 是否被@ */
const beMentioned = computed(() => {
return !!props.conversation.aitMsgs?.length
})
/** 是否展示 未读数 */
const showConversationUnread = computed(() => {
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
return (
props?.conversation?.lastMessage?.messageRefer.senderId ===
myUserAccountId &&
props?.conversation?.lastMessage?.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL &&
props?.conversation?.lastMessage?.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION &&
props?.conversation?.lastMessage?.sendingState ===
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED &&
props?.conversation?.lastMessage?.lastMessageState !==
V2NIMConst.V2NIMLastMessageState.V2NIM_MESSAGE_STATUS_REVOKE
)
} else {
return false
}
})
// 左滑显示 action 动画
let startX = 0,
startY = 0
// 开始左滑
function handleTouchStart(event: TouchEvent) {
startX = event.changedTouches[0].pageX
startY = event.changedTouches[0].pageY
}
// 左滑
function handleTouchMove(event: TouchEvent) {
const moveEndX = event.changedTouches[0].pageX
const moveEndY = event.changedTouches[0].pageY
const X = moveEndX - startX + 20
const Y = moveEndY - startY
if (Math.abs(X) > Math.abs(Y) && X > 0) {
emit('leftSlide', null)
} else if (Math.abs(X) > Math.abs(Y) && X < 0) {
emit('leftSlide', props.conversation)
}
}
/** 会话列表点击事件 */
function handleConversationItemClick() {
if (props.showMoreActions) {
emit('leftSlide', null)
return
}
emit('click', props.conversation)
}
/** 监听是否是Ai 数字人 */
const isAiUserWatch = autorun(() => {
isAiUser.value = uni.$UIKitStore.aiUserStore.isAIUser(to.value)
})
/** 监听会话方在线离线状态 */
const statusWatch = autorun(() => {
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
const stateMap = uni.$UIKitStore?.subscriptionStore.stateMap
if (
stateMap.get(to.value) &&
uni.$UIKitStore.localOptions.loginStateVisible
) {
status.value =
stateMap.get(to.value)?.statusType ===
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
} else {
status.value = false
}
}
})
onUnmounted(() => {
isAiUserWatch()
statusWatch()
})
</script>
<style lang="scss" scoped>
$cellHeight: 72px;
.conversation-item-container {
position: relative;
transition: transform 0.3s;
&.show-action-list {
transform: translateX(-200px);
}
&.stick-on-top {
background: #f3f5f7;
}
.beMentioned {
color: #ff4d4f;
}
.content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.right-action-list {
position: absolute;
top: 0;
right: -200px;
bottom: 0;
width: 200px;
white-space: nowrap;
.right-action-item {
width: 100px;
display: inline-block;
color: #fff;
text-align: center;
height: $cellHeight;
line-height: $cellHeight;
}
.action-top {
background: #337eff;
}
.action-delete {
background: #a8abb6;
}
}
.conversation-item-content {
display: flex;
align-items: center;
padding: 10px 20px;
height: $cellHeight;
box-sizing: border-box;
}
.conversation-item-left {
position: relative;
.conversation-item-badge {
position: absolute;
top: 0px;
right: 0px;
z-index: 10;
}
}
.conversation-item-right {
flex: 1;
width: 0;
margin-left: 10px;
}
.conversation-item-top {
display: flex;
justify-content: space-between;
align-items: center;
.conversation-item-title {
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
white-space: nowrap; //溢出不换行
}
.conversation-item-time {
font-size: 12px;
color: #cccccc;
text-align: right;
width: 90px;
flex-shrink: 0;
}
}
.conversation-item-desc {
font-size: 13px;
color: #999999;
display: flex;
align-items: center;
justify-content: space-between;
height: 22px;
.conversation-item-desc-content {
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
white-space: nowrap; //溢出不换行
flex: 1;
}
.conversation-item-desc-state {
margin-left: 10px;
}
}
.dot {
background-color: #ff4d4f;
color: #fff;
width: 10px;
height: 10px;
border-radius: 5px;
box-sizing: border-box;
z-index: 99;
}
.badge {
background-color: #ff4d4f;
color: #fff;
font-size: 12px;
min-width: 20px;
height: 20px;
line-height: 19px;
border-radius: 10px;
padding: 0 5px;
box-sizing: border-box;
text-align: center;
z-index: 99;
position: relative;
}
.unread {
position: absolute;
right: -4px;
top: -2px;
z-index: 99;
}
.conversation-item-desc-ait {
display: inline-block;
}
.login-state-icon {
width: 8px;
height: 8px;
box-sizing: content-box;
background-color: #84ed85;
border: 2px solid #fff;
position: absolute;
right: -2px;
bottom: -2px;
border-radius: 50%;
}
.unlogin-state-icon {
width: 8px;
height: 8px;
box-sizing: content-box;
background-color: #d4d9da;
border: 2px solid #fff;
position: absolute;
right: -2px;
bottom: -2px;
border-radius: 50%;
}
</style>