feat(im): 优化 ConversationItem.vue 增加相关评审

im
YunaiV 2026-04-27 08:22:14 +08:00
parent a1a9053aaa
commit 115e0482db
1 changed files with 217 additions and 0 deletions

View File

@ -0,0 +1,217 @@
<template>
<div
class="relative flex items-center gap-2.5 px-4 py-3 cursor-pointer transition-colors hover:bg-[var(--el-fill-color)]"
:class="{ '!bg-[#d9ecff] dark:!bg-[var(--el-color-primary-light-8)]': isActive }"
@click="handleClick"
@contextmenu.prevent="handleContextMenu"
>
<!-- 头像 + 未读徽标免打扰会话不显示徽标 -->
<div class="relative">
<UserAvatar
:url="conversation.avatar"
:name="conversation.name"
:size="40"
:clickable="false"
/>
<span
v-show="!conversation.muted && conversation.unreadCount > 0"
class="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1.5 text-11px leading-[18px] text-white text-center bg-[#f56c6c] border border-white dark:border-[var(--el-bg-color)] rounded-full box-border whitespace-nowrap"
>
{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
</span>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<span class="flex flex-1 items-center gap-1 min-w-0">
<span class="overflow-hidden text-sm truncate text-[var(--el-text-color-primary)]">
{{ conversation.name }}
</span>
<!-- TODO @AI不要有动效 -->
<el-tag
v-if="isGroup"
type="primary"
size="small"
effect="plain"
class="conversation-item__tag"
>
</el-tag>
</span>
<span class="flex-shrink-0 ml-1 text-12px text-[var(--el-text-color-secondary)]">
{{ formatTime(conversation.lastSendTime) }}
</span>
</div>
<div class="flex items-center mt-1 leading-5">
<!-- @红字提示atMe 优先于 atAll -->
<span v-if="atText" class="flex-shrink-0 text-12px text-[#c70b0b]">{{ atText }}</span>
<!-- 群聊最后一条发送者前缀 -->
<span
v-if="showSendName"
class="flex-shrink-0 text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap"
>
{{ conversation.senderNickName }}:&nbsp;
</span>
<span
class="flex-1 overflow-hidden text-12px truncate text-[var(--el-text-color-secondary)]"
>
{{ conversation.lastContent }}
</span>
<!-- 置顶 & 免打扰图标 -->
<el-icon
v-if="conversation.muted"
class="conversation-item__muted flex-shrink-0 ml-1 text-14px text-[var(--el-text-color-disabled)]"
title="消息免打扰"
>
<!-- TODO @AI消息免打扰后是个 / 铃铛 -->
<Bell />
</el-icon>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { Bell } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import { useConversationStore } from '../../../../store/conversationStore'
import { useFriendStore } from '../../../../store/friendStore'
import { useGroupStore } from '../../../../store/groupStore'
import { useImUiStore } from '../../../../store/uiStore'
import { ImConversationType, isNormalMessage } from '../../../../../utils/constants'
import type { Conversation } from '../../../../types'
import UserAvatar from '../../../../components/UserAvatar.vue'
defineOptions({ name: 'ImConversationItem' })
const props = defineProps<{
conversation: Conversation
}>()
const conversationStore = useConversationStore()
const friendStore = useFriendStore()
const groupStore = useGroupStore()
const uiStore = useImUiStore()
const isActive = computed(
() =>
conversationStore.activeConversation?.targetId === props.conversation.targetId &&
conversationStore.activeConversation?.type === props.conversation.type
)
const isGroup = computed(() => props.conversation.type === ImConversationType.GROUP)
// + +
// TODO @AI
const showSendName = computed(() => {
if (!isGroup.value) {
return false
}
if (!props.conversation.senderNickName) {
return false
}
const last = props.conversation.messages?.[props.conversation.messages.length - 1]
if (!last) {
return false
}
return isNormalMessage(last.type)
})
// "@ " / "@ "
// TODO @AI
const atText = computed(() => {
if (props.conversation.atMe) {
return '[有人@我]'
}
if (props.conversation.atAll) {
return '[@全体成员]'
}
return ''
})
function handleClick() {
conversationStore.setActiveConversation(props.conversation)
}
// / /
// TODO @AI
function handleContextMenu(e: MouseEvent) {
uiStore.openContextMenu(
{ x: e.clientX, y: e.clientY },
// TODO @AITOP/MUTED 线
[
{ key: 'TOP', name: props.conversation.top ? '取消置顶' : '置顶' },
// TODO @AI
{ key: 'MUTED', name: props.conversation.muted ? '新消息提醒' : '消息免打扰' },
{ key: 'DELETE', name: '删除会话' }
],
async (item) => {
// TODO @AIhandleXXX key
if (item.key === 'TOP') {
conversationStore.setTop(
props.conversation.type,
props.conversation.targetId,
!props.conversation.top
)
} else if (item.key === 'MUTED') {
// TODO /im/group/update muted groupStore.setMuted friendStore.setMuted /im/friend/update
// friendStore / groupStore
const next = !props.conversation.muted
conversationStore.setMuted(props.conversation.type, props.conversation.targetId, next)
// TODO @AI await
if (props.conversation.type === ImConversationType.PRIVATE) {
friendStore.setMuted(props.conversation.targetId, next)
} else {
groupStore.setMuted(props.conversation.targetId, next)
}
} else if (item.key === 'DELETE') {
try {
await ElMessageBox.confirm(
`确定删除与「${props.conversation.name}」的会话吗?`,
'删除会话',
{ type: 'warning' }
)
conversationStore.removeConversation(props.conversation.type, props.conversation.targetId)
} catch {
//
}
}
}
)
}
// TODO @AI format date
function formatTime(timestamp: number): string {
if (!timestamp) {
return ''
}
const date = new Date(timestamp)
const now = new Date()
const pad = (n: number) => n.toString().padStart(2, '0')
const isToday =
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate()
if (isToday) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`
}
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
</script>
<style scoped>
/* el-tag 内部尺寸走 CSS 变量UnoCSS 的高度/内边距会被 el-tag 自身的样式覆盖,用 :deep 微调 */
.conversation-item__tag {
flex-shrink: 0;
height: 18px;
padding: 0 4px;
line-height: 16px;
}
/* el-icon 的全局 color:var(--color) 在暗色模式下会渲染成白色,这里用 :deep + !important 锁定 */
.conversation-item__muted :deep(svg) {
fill: currentColor !important;
}
</style>