✨ feat(im): 优化 icon 的导入
parent
9fc10b304c
commit
6ead932813
|
|
@ -27,7 +27,7 @@
|
|||
class="mb-2.5"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
<Icon icon="ant-design:search-outlined" />
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
<div
|
||||
class="im-chat-group-side__tool-btn flex items-center justify-center w-[38px] h-[38px] text-[18px] cursor-pointer border border-dashed border-[var(--el-border-color)] rounded text-[var(--el-text-color-regular)] hover:text-[#409eff] hover:border-[#409eff]"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</div>
|
||||
<div class="mt-1 text-12px text-[var(--el-text-color-regular)]">邀请</div>
|
||||
</div>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
<div
|
||||
class="im-chat-group-side__tool-btn flex items-center justify-center w-[38px] h-[38px] text-[18px] cursor-pointer border border-dashed border-[var(--el-border-color)] rounded text-[var(--el-text-color-regular)] hover:text-[#409eff] hover:border-[#409eff]"
|
||||
>
|
||||
<el-icon><Minus /></el-icon>
|
||||
<Icon icon="ant-design:minus-outlined" />
|
||||
</div>
|
||||
<div class="mt-1 text-12px text-[var(--el-text-color-regular)]">移除</div>
|
||||
</div>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Minus } from '@element-plus/icons-vue'
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
|
|
|||
|
|
@ -11,28 +11,22 @@
|
|||
<div class="flex gap-3 items-center">
|
||||
<!-- 聊天历史:从输入区底部工具栏挪到顶部右上角,对齐微信 PC(点击弹窗承接历史消息) -->
|
||||
<el-tooltip content="聊天历史" placement="bottom">
|
||||
<el-icon
|
||||
class="chat-panel__header-icon text-[20px] cursor-pointer"
|
||||
<Icon
|
||||
icon="ant-design:profile-outlined"
|
||||
:size="20"
|
||||
class="chat-panel__header-icon cursor-pointer"
|
||||
@click="historyVisible = true"
|
||||
>
|
||||
<Tickets />
|
||||
</el-icon>
|
||||
/>
|
||||
</el-tooltip>
|
||||
<!-- TODO @AI:无论是群聊还是单聊,都是 *** 三个点 -->
|
||||
<!-- 私聊:聊天信息抽屉(免打扰 / 置顶) -->
|
||||
<el-tooltip v-if="!isGroup" content="聊天信息" placement="bottom">
|
||||
<el-icon
|
||||
class="chat-panel__header-icon text-[20px] cursor-pointer"
|
||||
@click="togglePrivateSide"
|
||||
>
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<!-- 群聊:群聊信息抽屉 -->
|
||||
<el-tooltip v-if="isGroup" content="群聊信息" placement="bottom">
|
||||
<el-icon class="chat-panel__header-icon text-[20px] cursor-pointer" @click="toggleSide">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
<!-- 信息抽屉入口:群聊 / 私聊统一用 3 点图标,对齐微信 PC;
|
||||
sideVisible 在群 / 私聊两个抽屉之间共用一个 ref,由 v-if-else 决定挂哪个 -->
|
||||
<el-tooltip :content="isGroup ? '群聊信息' : '聊天信息'" placement="bottom">
|
||||
<Icon
|
||||
icon="ant-design:more-outlined"
|
||||
:size="20"
|
||||
class="chat-panel__header-icon cursor-pointer"
|
||||
@click="toggleSide"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -67,7 +61,7 @@
|
|||
class="chat-panel__jump-bottom sticky bottom-3 left-1/2 inline-flex gap-1.5 items-center w-fit mx-auto px-3.5 py-1.5 text-xs text-[#409eff] bg-[var(--el-bg-color-overlay)] rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.12)] cursor-pointer hover:text-white hover:bg-[#409eff]"
|
||||
@click="scrollToBottom(true)"
|
||||
>
|
||||
<el-icon class="text-sm"><ArrowDown /></el-icon>
|
||||
<Icon icon="ant-design:down-outlined" :size="14" />
|
||||
<span v-if="newMessageCount > 0" class="font-medium">
|
||||
{{ newMessageCount > 99 ? '99+' : newMessageCount }} 条新消息
|
||||
</span>
|
||||
|
|
@ -94,7 +88,7 @@
|
|||
/>
|
||||
<ChatPrivateSide
|
||||
v-else
|
||||
v-model="privateSideVisible"
|
||||
v-model="sideVisible"
|
||||
:conversation="conversationStore.activeConversation"
|
||||
:friend="privateFriend"
|
||||
/>
|
||||
|
|
@ -113,7 +107,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, nextTick, computed } from 'vue'
|
||||
import { InfoFilled, MoreFilled, ArrowDown, Tickets } from '@element-plus/icons-vue'
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
|
||||
import { useConversationStore } from '../../../store/conversationStore'
|
||||
import { useFriendStore } from '../../../store/friendStore'
|
||||
|
|
@ -158,7 +152,13 @@ const showJumpToBottom = ref(false)
|
|||
/** 不在底部期间累计的新消息数 */
|
||||
const newMessageCount = ref(0)
|
||||
|
||||
// 当前激活的群详情:优先 groupStore;回落到 activeConversation 自身字段
|
||||
/**
|
||||
* 当前激活的群详情:优先 groupStore(带详细字段),未加载完时用 activeConversation 兜底
|
||||
*
|
||||
* groupStore 是按需懒加载的,初次进群时 ensureGroupData 触发后才会有完整数据;
|
||||
* 兜底字段(name / avatar)保证 header 不会"闪空",notice / ownerId / memberCount
|
||||
* 必须等 store 就位才有值(这些字段在 conversation 里没有)
|
||||
*/
|
||||
const groupInfo = computed<
|
||||
(GroupLite & { notice?: string; remarkNickName?: string; ownerId?: number }) | undefined
|
||||
>(() => {
|
||||
|
|
@ -166,16 +166,15 @@ const groupInfo = computed<
|
|||
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
||||
return undefined
|
||||
}
|
||||
// TODO @AI:group
|
||||
const g = groupStore.getGroup(conversation.targetId)
|
||||
const group = groupStore.getGroup(conversation.targetId)
|
||||
return {
|
||||
id: conversation.targetId,
|
||||
name: g?.name || conversation.name,
|
||||
showGroupName: g?.name || conversation.name,
|
||||
showImage: g?.avatar || conversation.avatar,
|
||||
notice: g?.notice,
|
||||
ownerId: g?.ownerUserId,
|
||||
memberCount: g?.memberCount
|
||||
name: group?.name || conversation.name,
|
||||
showGroupName: group?.name || conversation.name,
|
||||
showImage: group?.avatar || conversation.avatar,
|
||||
notice: group?.notice,
|
||||
ownerId: group?.ownerUserId,
|
||||
memberCount: group?.memberCount
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -194,36 +193,40 @@ const groupMembers = computed<GroupMemberLite[]>(() => {
|
|||
}))
|
||||
})
|
||||
|
||||
/** 好友列表(用于邀请对话框) */
|
||||
/** 好友列表(用于"邀请入群"对话框):把 friendStore 的全量好友 map 成 FriendLite 窄接口 */
|
||||
const groupFriends = computed<FriendLite[]>(() =>
|
||||
// TODO @AI:friend
|
||||
friendStore.getActiveFriends.map((f) => ({
|
||||
id: f.friendUserId,
|
||||
nickname: f.nickname,
|
||||
avatar: f.avatar,
|
||||
deleted: f.status === CommonStatusEnum.DISABLE
|
||||
friendStore.getActiveFriends.map((friend) => ({
|
||||
id: friend.friendUserId,
|
||||
nickname: friend.nickname,
|
||||
avatar: friend.avatar,
|
||||
deleted: friend.status === CommonStatusEnum.DISABLE
|
||||
}))
|
||||
)
|
||||
|
||||
// TODO @AI:注释格式不对。
|
||||
// 切换到群会话时,自动从后端拉取 group / members / 好友(带缓存)
|
||||
//
|
||||
// 三件事各自 fire-and-forget + 各自 catch:之前用 Promise.all 时任意一项失败会让其它
|
||||
// 已成功的结果只记一条笼统日志,丢掉具体出错点。这里拆开,谁挂谁单独记,不互相牵连
|
||||
/**
|
||||
* 切换到群会话时,自动从后端拉取 group / members / 好友(store 内自带缓存)
|
||||
*
|
||||
* 三件事各自 fire-and-forget + 各自 catch:之前用 Promise.all 时任意一项失败会让其它
|
||||
* 已成功的结果只记一条笼统日志,丢掉具体出错点。这里拆开,谁挂谁单独记,不互相牵连。
|
||||
* 错误日志把 groupId 一起带上,多群环境下排查问题能直接定位
|
||||
*/
|
||||
function ensureGroupData(groupId: number) {
|
||||
// TODO @AI:错误日志,应该把 groupid 带上;
|
||||
groupStore.loadGroupInfo(groupId).catch((e) => {
|
||||
console.warn('[IM ChatPanel] loadGroupInfo 失败', e)
|
||||
groupStore.loadGroupInfo(groupId).catch((error) => {
|
||||
console.warn('[IM ChatPanel] loadGroupInfo 失败', { groupId }, error)
|
||||
})
|
||||
groupStore.loadGroupMembers(groupId).catch((e) => {
|
||||
console.warn('[IM ChatPanel] loadGroupMembers 失败', e)
|
||||
groupStore.loadGroupMembers(groupId).catch((error) => {
|
||||
console.warn('[IM ChatPanel] loadGroupMembers 失败', { groupId }, error)
|
||||
})
|
||||
friendStore.loadFriends().catch((e) => {
|
||||
console.warn('[IM ChatPanel] loadFriends 失败', e)
|
||||
friendStore.loadFriends().catch((error) => {
|
||||
console.warn('[IM ChatPanel] loadFriends 失败', { groupId }, error)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO @AI:注释
|
||||
/**
|
||||
* 群信息抽屉里点"刷新"等触发:强拉一次最新群元数据 + 群成员(force=true 跳过缓存)
|
||||
*
|
||||
* 仅在当前会话仍是同一个群时执行,避免 await 期间用户已经切走、把别的群数据也跟着重拉
|
||||
*/
|
||||
function reloadGroupData() {
|
||||
const conversation = conversationStore.activeConversation
|
||||
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
||||
|
|
@ -234,22 +237,19 @@ function reloadGroupData() {
|
|||
}
|
||||
|
||||
const historyVisible = ref(false)
|
||||
// TODO @AI:sideVisible 是不是可以包含 sideVisible + privateSideVisible
|
||||
/** 群聊抽屉的开关:纯 ChatPanel 本地 UI 状态 */
|
||||
const sideVisible = ref(false) // 群聊抽屉的开关:纯 ChatPanel 本地 UI 状态
|
||||
/** 私聊抽屉的开关:纯 ChatPanel 本地 UI 状态 */
|
||||
const privateSideVisible = ref(false)
|
||||
/**
|
||||
* 信息抽屉的开关(纯 ChatPanel 本地 UI 状态)
|
||||
*
|
||||
* 群聊 / 私聊共用一个 ref:模板里 v-if-else 决定挂哪个抽屉,同一时刻只有一个组件
|
||||
* 在 DOM 里,所以一个布尔够用。早先拆成 sideVisible + privateSideVisible 是冗余
|
||||
*/
|
||||
const sideVisible = ref(false)
|
||||
|
||||
// TODO @AI:注释
|
||||
/** 信息抽屉的 toggle:跟 header 上 3 点图标按钮共用 */
|
||||
function toggleSide() {
|
||||
sideVisible.value = !sideVisible.value
|
||||
}
|
||||
|
||||
// TODO @AI:注释
|
||||
function togglePrivateSide() {
|
||||
privateSideVisible.value = !privateSideVisible.value
|
||||
}
|
||||
|
||||
/** 当前私聊对应的好友(抽屉头部展示用) */
|
||||
const privateFriend = computed(() => {
|
||||
const conversation = conversationStore.activeConversation
|
||||
|
|
@ -268,7 +268,11 @@ function distanceFromBottom(): number {
|
|||
return el.scrollHeight - el.scrollTop - el.clientHeight
|
||||
}
|
||||
|
||||
// TODO @AI:注释
|
||||
/**
|
||||
* 消息列表滚动事件:刷新"是否在底部"状态
|
||||
* - 在底部:隐藏"回到底部"浮窗 + 清掉"未读新消息"计数
|
||||
* - 不在底部:显示"回到底部"浮窗,新消息会累计到 newMessageCount
|
||||
*/
|
||||
function handleScroll() {
|
||||
const dist = distanceFromBottom()
|
||||
const atBottom = dist <= BOTTOM_THRESHOLD
|
||||
|
|
@ -278,7 +282,13 @@ function handleScroll() {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO @AI:注释
|
||||
/**
|
||||
* 滚到底部:切会话 / 收到新消息(且当前在底部)/ 用户主动点"回到底部" 都走这里
|
||||
*
|
||||
* 包 nextTick 是为了等 v-for 把新消息真正渲染进 DOM 后再算 scrollHeight,
|
||||
* 否则可能滚到的还是旧高度(差最后一条的位置)。smooth=true 走平滑动画,
|
||||
* 适合用户主动点击;初始 / 自动滚动用 auto,避免用户感知到动画拖拽
|
||||
*/
|
||||
function scrollToBottom(smooth = false) {
|
||||
nextTick(() => {
|
||||
if (!listRef.value) {
|
||||
|
|
@ -321,18 +331,20 @@ async function handleLocate(messageId: number) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 消息变化时:
|
||||
* - 如果当前在底部,自动跟进滚动
|
||||
* - 否则累计 newMessageCount
|
||||
* 消息数量变化时的滚动跟进策略
|
||||
* - 当前在底部 → 自动滚到新底部(用户在"实时跟读",体验上跟微信一致)
|
||||
* - 不在底部 → 累计 newMessageCount,让 sticky 浮窗显示"X 条新消息",让用户主动点
|
||||
*/
|
||||
watch(
|
||||
() => messages.value.length,
|
||||
(newLen, oldLen) => {
|
||||
// 仅处理新增(delta > 0);删除 / 撤回让 length 减少时不动滚动状态
|
||||
const delta = (newLen || 0) - (oldLen || 0)
|
||||
if (delta <= 0) {
|
||||
return
|
||||
}
|
||||
// TODO @AI:代码块里的注释。
|
||||
// 用 BOTTOM_THRESHOLD(80px)做容差:用户稍微往上翻几行就视作"不在底部",
|
||||
// 否则一直 auto-scroll 会把人正在读的内容顶走,体验糟糕
|
||||
const dist = distanceFromBottom()
|
||||
if (dist <= BOTTOM_THRESHOLD) {
|
||||
scrollToBottom()
|
||||
|
|
@ -343,14 +355,19 @@ watch(
|
|||
}
|
||||
)
|
||||
|
||||
// 切换会话:强制滚到底部,并清零累计;若是群会话则预拉群资料
|
||||
/**
|
||||
* 切换会话:清空"在不在底部"相关状态、强制滚到底部、群会话预拉资料
|
||||
*
|
||||
* immediate:true 让首次进入页面就能正确初始化(无需等到第一次切换)
|
||||
*/
|
||||
watch(
|
||||
() => conversationStore.activeConversation?.targetId,
|
||||
(targetId) => {
|
||||
// TODO @AI:代码块里的注释。
|
||||
// 切群时上一会话的"未读累计 + 浮窗显示"必须清掉,否则会带到新会话里看起来很突兀
|
||||
newMessageCount.value = 0
|
||||
showJumpToBottom.value = false
|
||||
scrollToBottom()
|
||||
// 仅群聊预拉详情 / 成员 / 好友(私聊只需 friendStore 里的对端,已经 globally pull 了)
|
||||
if (targetId && conversationStore.activeConversation?.type === ImConversationType.GROUP) {
|
||||
ensureGroupData(targetId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@
|
|||
>
|
||||
{{ filterChipLabel }}
|
||||
</el-tag>
|
||||
<el-icon v-else class="text-[var(--el-text-color-secondary)]"><Search /></el-icon>
|
||||
<Icon
|
||||
v-else
|
||||
icon="ant-design:search-outlined"
|
||||
class="text-[var(--el-text-color-secondary)]"
|
||||
/>
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
|
|
@ -109,7 +113,7 @@
|
|||
<div>
|
||||
<el-input v-model="memberSearchKeyword" placeholder="搜索群成员" size="small">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
<Icon icon="ant-design:search-outlined" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="max-h-[360px] overflow-y-auto mt-2">
|
||||
|
|
@ -293,7 +297,6 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
|
|
|
|||
|
|
@ -120,12 +120,12 @@
|
|||
]"
|
||||
@click="handleVoiceClick"
|
||||
>
|
||||
<el-icon
|
||||
class="message-bubble__voice-icon !text-[18px]"
|
||||
<Icon
|
||||
icon="ant-design:audio-outlined"
|
||||
:size="18"
|
||||
class="message-bubble__voice-icon"
|
||||
:class="{ 'im-voice-playing': voicePlaying }"
|
||||
>
|
||||
<Microphone />
|
||||
</el-icon>
|
||||
/>
|
||||
<span class="text-13px text-[var(--el-text-color-primary)]">
|
||||
{{ formatSeconds(voicePayload.duration) }}
|
||||
</span>
|
||||
|
|
@ -158,18 +158,19 @@
|
|||
<!-- 状态区:自己消息展示发送状态 + 已读/群回执;对方消息 + @自己时展示 @徽标 -->
|
||||
<div class="flex gap-1.5 items-center text-base">
|
||||
<template v-if="message.selfSend">
|
||||
<el-icon v-if="message.status === ImMessageStatus.SENDING" class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
<Icon
|
||||
v-if="message.status === ImMessageStatus.SENDING"
|
||||
icon="ant-design:loading-outlined"
|
||||
class="im-loading-spin"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="message.status === ImMessageStatus.FAILED"
|
||||
icon="ant-design:warning-filled"
|
||||
color="#f56c6c"
|
||||
class="cursor-pointer"
|
||||
title="发送失败,点击重试"
|
||||
@click="handleResend"
|
||||
>
|
||||
<WarningFilled />
|
||||
</el-icon>
|
||||
/>
|
||||
<!-- 已读态(私聊) -->
|
||||
<span
|
||||
v-else-if="privateReadLabel"
|
||||
|
|
@ -210,7 +211,6 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { Loading, WarningFilled, Microphone } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
|
|
@ -622,4 +622,14 @@ function handleDelete() {
|
|||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* SENDING 状态的转圈动画:el-icon 自带 .is-loading 旋转,迁到 Iconify 后丢了,自己补一份 */
|
||||
.im-loading-spin {
|
||||
animation: im-loading-spin 1s linear infinite;
|
||||
}
|
||||
@keyframes im-loading-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue