✨ feat(im):增加好友删除时,增加是否删除本地聊天的选项
parent
1400bd80dd
commit
2a55748296
|
|
@ -38,8 +38,11 @@ export const getFriend = (friendUserId: number | string) => {
|
|||
}
|
||||
|
||||
// 删除好友(单向软删除)
|
||||
export const deleteFriend = (friendUserId: number | string) => {
|
||||
return request.delete<boolean>({ url: '/im/friend/delete', params: { friendUserId } })
|
||||
export const deleteFriend = (friendUserId: number | string, clear: boolean) => {
|
||||
return request.delete<boolean>({
|
||||
url: '/im/friend/delete',
|
||||
params: { friendUserId, clear }
|
||||
})
|
||||
}
|
||||
|
||||
// 更新好友信息(备注 / 免打扰 / 联系人置顶)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const props = withDefaults(
|
|||
presetUser?: UserVO | null
|
||||
/** 添加来源;参见 ImFriendAddSourceEnum */
|
||||
addSource?: number
|
||||
/** 来源附带信息:addSource=2(群聊)时传群名,话术拼为「我是 XX 群的 YY」 */
|
||||
/** 来源附带信息:addSource=ImFriendAddSource.GROUP 时传群名,话术拼为「我是 XX 群的 YY」 */
|
||||
addSourceExtra?: string
|
||||
}>(),
|
||||
{
|
||||
|
|
@ -175,7 +175,9 @@ const message = useMessage()
|
|||
const currentUserId = computed(() => getCurrentUserId())
|
||||
|
||||
/** 搜索结果过滤掉自己;用 v-if 而非 v-show,避免 DOM 占位 + 头像无效请求 */
|
||||
const visibleUsers = computed(() => users.value.filter((u) => u.id !== currentUserId.value))
|
||||
const visibleUsers = computed(() =>
|
||||
users.value.filter((user) => user.id !== currentUserId.value)
|
||||
)
|
||||
const keyword = ref('')
|
||||
const users = ref<UserVO[]>([])
|
||||
const searched = ref(false)
|
||||
|
|
|
|||
|
|
@ -171,8 +171,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import type { InputInstance } from 'element-plus'
|
||||
import { computed, h, nextTick, ref, watch } from 'vue'
|
||||
import { ElCheckbox, ElMessageBox, type InputInstance } from 'element-plus'
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
|
||||
|
|
@ -373,16 +373,39 @@ async function handleUnblock() {
|
|||
message.success('已移出黑名单')
|
||||
}
|
||||
|
||||
/** 删除联系人:confirm → friendStore.deleteFriend(内部级联清会话)→ 通知父级关浮层 / 清选中 */
|
||||
/** 删除联系人:弹自定义确认(含「同时清空聊天记录」选项)→ friendStore.deleteFriend → 通知父级关浮层 / 清选中 */
|
||||
async function handleDeleteFriend() {
|
||||
if (!props.user?.id) {
|
||||
return
|
||||
}
|
||||
const target = props.user
|
||||
// 二次确认
|
||||
await message.confirm(`确定删除好友「${target.nickname || ''}」吗?`, '删除联系人')
|
||||
// 删除好友
|
||||
await friendStore.deleteFriend(target.id)
|
||||
const clearConversation = ref(true)
|
||||
try {
|
||||
await ElMessageBox({
|
||||
title: '删除联系人',
|
||||
message: () =>
|
||||
h('div', { class: 'flex flex-col gap-3 text-sm' }, [
|
||||
h('div', `确定删除好友「${target.nickname || ''}」?`),
|
||||
h(
|
||||
ElCheckbox,
|
||||
{
|
||||
modelValue: clearConversation.value,
|
||||
'onUpdate:modelValue': (value: boolean) => {
|
||||
clearConversation.value = value
|
||||
}
|
||||
},
|
||||
() => '同时清空聊天记录'
|
||||
)
|
||||
]),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonClass: 'el-button--danger'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await friendStore.deleteFriend(target.id, clearConversation.value)
|
||||
message.success('已删除好友')
|
||||
emit('deleted', target)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,20 @@
|
|||
:group-id="conversationStore.activeConversation.targetId"
|
||||
@locate="handleLocate"
|
||||
/>
|
||||
<!-- 私聊:对方不再是好友(删了 / 被删了);胶囊嵌在 header 内(跟群置顶同级),点击弹 UserInfoCard -->
|
||||
<div v-if="showNotFriendBanner" class="message-panel__not-friend-container">
|
||||
<div class="message-panel__not-friend" @click="handleNotFriendClick">
|
||||
<span class="message-panel__not-friend-icon">
|
||||
<Icon icon="ant-design:user-outlined" :size="11" />
|
||||
</span>
|
||||
<span>对方还不是你的朋友</span>
|
||||
<Icon
|
||||
icon="ep:arrow-right"
|
||||
:size="12"
|
||||
class="text-[var(--el-text-color-secondary)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:消息列表 -->
|
||||
|
|
@ -150,6 +164,7 @@ import { useMessage } from '@/hooks/web/useMessage'
|
|||
|
||||
import { useConversationStore } from '../../../../store/conversationStore'
|
||||
import { useFriendStore } from '../../../../store/friendStore'
|
||||
import { useImUiStore } from '../../../../store/uiStore'
|
||||
import { getMemberDisplayName } from '@/views/im/utils/user'
|
||||
import { useGroupStore } from '../../../../store/groupStore'
|
||||
import { ImConversationType } from '@/views/im/utils/constants'
|
||||
|
|
@ -167,6 +182,7 @@ defineOptions({ name: 'ImMessagePanel' })
|
|||
|
||||
const conversationStore = useConversationStore()
|
||||
const friendStore = useFriendStore()
|
||||
const uiStore = useImUiStore()
|
||||
const groupStore = useGroupStore()
|
||||
const message = useMessage()
|
||||
const listRef = ref<HTMLElement>()
|
||||
|
|
@ -176,6 +192,32 @@ const isGroup = computed(
|
|||
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
|
||||
)
|
||||
|
||||
/** 私聊会话且对端不是有效好友(删了 / 被删了),顶部展示「对方还不是你的朋友」黄色横幅 */
|
||||
const showNotFriendBanner = computed(() => {
|
||||
const conversation = conversationStore.activeConversation
|
||||
if (!conversation || conversation.type !== ImConversationType.PRIVATE) {
|
||||
return false
|
||||
}
|
||||
return !friendStore.isFriend(conversation.targetId)
|
||||
})
|
||||
|
||||
/** 点击「对方还不是你的朋友」胶囊:打开 UserInfoCard,引导用户重新添加 */
|
||||
function handleNotFriendClick(event: MouseEvent) {
|
||||
const conversation = conversationStore.activeConversation
|
||||
if (!conversation) {
|
||||
return
|
||||
}
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
uiStore.openUserInfoCard(
|
||||
{
|
||||
id: conversation.targetId,
|
||||
nickname: conversation.name,
|
||||
avatar: conversation.avatar
|
||||
},
|
||||
{ x: rect.left, y: rect.bottom + 4 }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 群聊 header 显示的人数:优先 groupStore.memberCount(无需等成员列表),无值再回退 members.length
|
||||
*
|
||||
|
|
@ -509,6 +551,42 @@ watch(
|
|||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
/* 「对方还不是你的朋友」胶囊:嵌在 header 内(跟群置顶同级),不占整行;padding 跟群置顶 padding 对齐 */
|
||||
.message-panel__not-friend-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 0 16px 8px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
.message-panel__not-friend {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-color-warning-light-9);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.message-panel__not-friend:hover {
|
||||
background-color: var(--el-color-warning-light-8);
|
||||
}
|
||||
/* 圆形小图标,深黄底色配白色 icon —— 跟微信一致的视觉锤 */
|
||||
.message-panel__not-friend-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background-color: var(--el-color-warning);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* sticky + translate 居中:fit-content 宽度不会撑满,transform 做水平 -50% 偏移;
|
||||
UnoCSS 表达 transform+transition 多 value 不太方便,这里用最小的 scoped CSS 承接 */
|
||||
.message-panel__jump-bottom {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export interface FriendNotificationPayload {
|
|||
displayName?: string
|
||||
muted?: boolean
|
||||
pinned?: boolean
|
||||
// FRIEND_DELETE:是否级联清理本端相关数据(如私聊会话)
|
||||
clear?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -142,7 +144,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
|
||||
// ==================== 远端拉取 ====================
|
||||
|
||||
/** 从后端拉取并覆盖本地列表;同步刷新对应私聊会话的展示名 / 头像 + 落 IDB;pending 期间复用同一 Promise */
|
||||
/** 从后端拉取并覆盖本地列表(含 DISABLE 历史好友给已删对话兜底);只同步 ENABLE 的会话信息,DISABLE 的不动 —— cascade 清会话由 WS dispatcher 按 payload.clear 处理,避免 fetchFriends 覆盖用户「不清空聊天记录」的选择 */
|
||||
async fetchFriends(force = false) {
|
||||
if (this.loaded && !force) {
|
||||
return
|
||||
|
|
@ -154,16 +156,17 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
.then((list) => {
|
||||
this.friends = (list || []).map(convertFriend)
|
||||
this.loaded = true
|
||||
// 同步 conversationStore 私聊会话的展示名 / 头像 / 免打扰
|
||||
const conversationStore = useConversationStore()
|
||||
for (const friend of this.friends) {
|
||||
if (friend.status === CommonStatusEnum.DISABLE) {
|
||||
continue
|
||||
}
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
|
||||
name: getFriendDisplayName(friend),
|
||||
avatar: friend.avatar,
|
||||
muted: friend.muted
|
||||
})
|
||||
}
|
||||
// 落本地缓存
|
||||
this.saveFriends()
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -195,27 +198,31 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
/** 同意一条好友申请;后端会双向落库 + 推 FRIEND_ADD,本端等通知到达再 upsertFriend */
|
||||
async agreeFriendRequest(requestId: number) {
|
||||
await apiAgreeFriendRequest(requestId)
|
||||
const request = this.findFriendRequest(requestId)
|
||||
if (request) {
|
||||
request.handleResult = ImFriendRequestHandleResult.AGREED
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
// 本地列表没这条,按 id 单查兜底
|
||||
await this.loadFriendRequest(requestId)
|
||||
}
|
||||
await this.applyHandleResult(requestId, ImFriendRequestHandleResult.AGREED)
|
||||
},
|
||||
|
||||
/** 拒绝一条好友申请 */
|
||||
async refuseFriendRequest(requestId: number, handleContent?: string) {
|
||||
await apiRefuseFriendRequest(requestId, handleContent)
|
||||
await this.applyHandleResult(requestId, ImFriendRequestHandleResult.REFUSED, handleContent)
|
||||
},
|
||||
|
||||
/** 把 handleResult 应用到本地申请记录;找不到就按 id 单查兜底 upsert,避免破坏 id 倒序 */
|
||||
async applyHandleResult(
|
||||
requestId: number,
|
||||
result: number,
|
||||
handleContent?: string
|
||||
): Promise<void> {
|
||||
const request = this.findFriendRequest(requestId)
|
||||
if (request) {
|
||||
request.handleResult = ImFriendRequestHandleResult.REFUSED
|
||||
request.handleContent = handleContent
|
||||
request.handleResult = result
|
||||
if (handleContent !== undefined) {
|
||||
request.handleContent = handleContent
|
||||
}
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
await this.loadFriendRequest(requestId)
|
||||
return
|
||||
}
|
||||
await this.loadFriendRequest(requestId)
|
||||
},
|
||||
|
||||
/** 拉取「我相关」的好友申请列表首页(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新);pending 期间复用同一 Promise */
|
||||
|
|
@ -290,10 +297,10 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
|
||||
// ==================== 好友关系操作 ====================
|
||||
|
||||
/** 删除好友(单向软删,本端置 DISABLE;级联清理本地私聊会话) */
|
||||
async deleteFriend(friendUserId: number) {
|
||||
await apiDeleteFriend(friendUserId)
|
||||
this.removeFriend(friendUserId)
|
||||
/** 删除好友(单向软删,本端置 DISABLE);clear=true 时级联清理本地相关数据(如私聊会话),并透传后端给多端同步 */
|
||||
async deleteFriend(friendUserId: number, clear: boolean = true) {
|
||||
await apiDeleteFriend(friendUserId, clear)
|
||||
this.removeFriend(friendUserId, clear)
|
||||
},
|
||||
|
||||
/** 切换免打扰:同步会话的 muted 字段,避免会话列表 muted 图标等 1210 推到才更新 */
|
||||
|
|
@ -379,17 +386,18 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
this.saveFriends()
|
||||
},
|
||||
|
||||
/** 本地标记删除(WebSocket FRIEND_DELETE 事件触发;同时级联清私聊会话) */
|
||||
removeFriend(friendUserId: number) {
|
||||
/** 本地标记删除(WebSocket FRIEND_DELETE 事件触发;clear=true 时级联清相关数据如私聊会话) */
|
||||
removeFriend(friendUserId: number, clear: boolean = true) {
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
// blocked 不动,跟后端 deleteFriend0「删好友期间保留拉黑状态」对齐
|
||||
friend.status = CommonStatusEnum.DISABLE
|
||||
friend.deleteTime = Date.now()
|
||||
}
|
||||
// 级联清理:把对应的私聊会话也软删,避免会话列表里留着已删好友
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.removePrivateConversation(friendUserId)
|
||||
if (clear) {
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.removePrivateConversation(friendUserId)
|
||||
}
|
||||
this.saveFriends()
|
||||
},
|
||||
|
||||
|
|
@ -417,26 +425,16 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
|
||||
/** FRIEND_REQUEST_APPROVED(1201):我的申请被同意;按 requestId 更新状态(FRIEND_ADD 会另外推) */
|
||||
applyFriendRequestApprovedNotification(payload: FriendNotificationPayload) {
|
||||
const request = this.findFriendRequest(payload.requestId!)
|
||||
if (request) {
|
||||
request.handleResult = ImFriendRequestHandleResult.AGREED
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
// 本地列表可能没这条(例如刚登录还没进 contact 页),按 id 单查兜底
|
||||
void this.loadFriendRequest(payload.requestId!)
|
||||
}
|
||||
void this.applyHandleResult(payload.requestId!, ImFriendRequestHandleResult.AGREED)
|
||||
},
|
||||
|
||||
/** FRIEND_REQUEST_REJECTED(1202):我的申请被拒绝;按 requestId 更新状态 */
|
||||
applyFriendRequestRejectedNotification(payload: FriendNotificationPayload) {
|
||||
const request = this.findFriendRequest(payload.requestId!)
|
||||
if (request) {
|
||||
request.handleResult = ImFriendRequestHandleResult.REFUSED
|
||||
request.handleContent = payload.handleContent
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
void this.loadFriendRequest(payload.requestId!)
|
||||
}
|
||||
void this.applyHandleResult(
|
||||
payload.requestId!,
|
||||
ImFriendRequestHandleResult.REFUSED,
|
||||
payload.handleContent
|
||||
)
|
||||
},
|
||||
|
||||
/** FRIEND_ADD(1204):新增好友;本端拉取好友详情并入库 */
|
||||
|
|
@ -444,9 +442,9 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
void this.loadFriendInfo(payload.friendUserId)
|
||||
},
|
||||
|
||||
/** FRIEND_DELETE(1205):好友被删除;本端清理 + 级联会话 */
|
||||
/** FRIEND_DELETE(1205):好友被删除;本端清理 + 按 payload.clear 决定是否级联清会话(多端跟主操作端一致) */
|
||||
applyFriendDeleteNotification(payload: FriendNotificationPayload) {
|
||||
this.removeFriend(payload.friendUserId)
|
||||
this.removeFriend(payload.friendUserId, payload.clear !== false)
|
||||
},
|
||||
|
||||
/** FRIEND_BLOCK(1207):拉黑;多端同步 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue