✨ feat(im): 优化 ConversationItem.vue 增加相关评审
parent
a1a9053aaa
commit
115e0482db
|
|
@ -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 }}:
|
||||||
|
</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 @AI:TOP/MUTED 下面的删除,可以加个横线,类似微信。然后颜色是红色么?【删除会话,简化为删除】
|
||||||
|
[
|
||||||
|
{ key: 'TOP', name: props.conversation.top ? '取消置顶' : '置顶' },
|
||||||
|
// TODO @AI:消息免打扰、允许消息通知。
|
||||||
|
{ key: 'MUTED', name: props.conversation.muted ? '新消息提醒' : '消息免打扰' },
|
||||||
|
{ key: 'DELETE', name: '删除会话' }
|
||||||
|
],
|
||||||
|
async (item) => {
|
||||||
|
// TODO @AI:是不是抽成小方法。handleXXX;下面的每个 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>
|
||||||
Loading…
Reference in New Issue