feat(im):将"免打扰"字段从 muted 全量重命名为 silent(DO/VO/Service/Mapper/测试/SQL + 前端 types/store/组件/管理后台),为后续 mute 禁言功能腾出词族
parent
dd75c702db
commit
4d006f8e73
|
|
@ -4,7 +4,7 @@ import request from '@/config/axios'
|
|||
export interface ImFriendRespVO {
|
||||
id: number // 关系记录编号
|
||||
friendUserId: number // 好友的用户编号
|
||||
muted?: boolean // 是否免打扰
|
||||
silent?: boolean // 是否免打扰
|
||||
displayName?: string // 好友展示备注(仅自己可见)
|
||||
displayNamePinyin?: string // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
|
||||
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
|
||||
|
|
@ -22,7 +22,7 @@ export interface ImFriendRespVO {
|
|||
// IM 好友更新 Request VO
|
||||
export interface ImFriendUpdateReqVO {
|
||||
friendUserId: number // 好友的用户编号
|
||||
muted?: boolean // 是否免打扰
|
||||
silent?: boolean // 是否免打扰
|
||||
displayName?: string // 好友展示备注
|
||||
pinned?: boolean // 是否置顶联系人
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface ImGroupMemberRespVO {
|
|||
userId: number // 用户编号
|
||||
displayUserName?: string // 组内显示名(群主设置的备注)
|
||||
groupRemark?: string // 群备注(当前用户对群的备注)
|
||||
muted?: boolean // 是否免打扰
|
||||
silent?: boolean // 是否免打扰
|
||||
status?: number // 成员状态(0=在群,1=退群)
|
||||
role?: number // 成员角色,参见 ImGroupMemberRole 枚举
|
||||
joinTime?: string // 入群时间
|
||||
|
|
@ -35,7 +35,7 @@ export interface ImGroupMemberUpdateReqVO {
|
|||
groupId: number // 群编号
|
||||
displayUserName?: string // 群内昵称
|
||||
groupRemark?: string // 群备注
|
||||
muted?: boolean // 是否免打扰
|
||||
silent?: boolean // 是否免打扰
|
||||
}
|
||||
|
||||
// 邀请用户加入群
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface ImManagerFriendVO {
|
|||
friendUserId: number
|
||||
friendNickname?: string
|
||||
displayName?: string
|
||||
muted: boolean
|
||||
silent: boolean
|
||||
status: number
|
||||
addTime?: Date
|
||||
deleteTime?: Date
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface ImManagerGroupMemberVO {
|
|||
avatar?: string
|
||||
displayUserName?: string
|
||||
groupRemark?: string
|
||||
muted?: boolean
|
||||
silent?: boolean
|
||||
status: number
|
||||
role?: number // 成员角色,参见 ImGroupMemberRole 枚举
|
||||
joinTime?: Date
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function handleSendMessage() {
|
|||
ImConversationType.PRIVATE,
|
||||
conversationName,
|
||||
user.value.avatar || '',
|
||||
{ muted: !!friend?.muted }
|
||||
{ silent: !!friend?.silent }
|
||||
)
|
||||
// 跳转会话 Tab(如果不在的话),并且不管当前路由是啥都先切到会话列表页
|
||||
if (router.currentRoute.value.name !== 'ImHomeConversation') {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ function pickFirstVisibleConversation(sorted: Conversation[]): Conversation | un
|
|||
if (pinnedExpanded) {
|
||||
return sorted[0]
|
||||
}
|
||||
return sorted.find((c) => !c.top || (!c.muted && (c.unreadCount || 0) > 0)) ?? sorted[0]
|
||||
return sorted.find((c) => !c.top || (!c.silent && (c.unreadCount || 0) > 0)) ?? sorted[0]
|
||||
}
|
||||
|
||||
/** 标签关闭前 flush 草稿队列;debounce 默认 trail-edge 触发,最后一次输入可能还压在队列里 */
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ function handleChatPeer(peerUserId: number) {
|
|||
ImConversationType.PRIVATE,
|
||||
conversationName,
|
||||
friend?.avatar || '',
|
||||
{ muted: !!friend?.muted }
|
||||
{ silent: !!friend?.silent }
|
||||
)
|
||||
router.push({ name: 'ImHomeConversation' })
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ function handleChatFriend(friend: FriendLite) {
|
|||
ImConversationType.PRIVATE,
|
||||
conversationName,
|
||||
friend.avatar || '',
|
||||
{ muted: !!entry?.muted }
|
||||
{ silent: !!entry?.silent }
|
||||
)
|
||||
router.push({ name: 'ImHomeConversation' })
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ function handleChatGroup(group: GroupLite) {
|
|||
ImConversationType.GROUP,
|
||||
group.showGroupName || group.name || '',
|
||||
group.showImage || group.showImageThumb || '',
|
||||
{ muted: !!entry?.muted }
|
||||
{ silent: !!entry?.silent }
|
||||
)
|
||||
router.push({ name: 'ImHomeConversation' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@
|
|||
<div class="im-conversation-group-side__section">
|
||||
<div class="im-conversation-group-side__row">
|
||||
<span class="im-conversation-group-side__label">消息免打扰</span>
|
||||
<el-switch :model-value="!!conversation?.muted" @change="onMutedChange" />
|
||||
<el-switch :model-value="!!conversation?.silent" @change="onMutedChange" />
|
||||
</div>
|
||||
<div class="im-conversation-group-side__row">
|
||||
<span class="im-conversation-group-side__label">置顶聊天</span>
|
||||
|
|
@ -549,7 +549,7 @@ async function saveRemark() {
|
|||
// ==================== 开关切换 ====================
|
||||
|
||||
/**
|
||||
* 消息免打扰:本地 conversationStore 立即切;后端 /muted 异步同步,失败回滚本地
|
||||
* 消息免打扰:本地 conversationStore 立即切;后端 /silent 异步同步,失败回滚本地
|
||||
*
|
||||
* 与 ConversationItem 右键菜单的"消息免打扰"语义一致;区别仅在 UI 入口
|
||||
*/
|
||||
|
|
@ -559,10 +559,10 @@ function onMutedChange(value: boolean | string | number) {
|
|||
}
|
||||
const next = !!value
|
||||
const { type, targetId } = props.conversation
|
||||
conversationStore.setMuted(type, targetId, next)
|
||||
groupStore.setMuted(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationGroupSide] setMuted 失败', { targetId }, error)
|
||||
conversationStore.setMuted(type, targetId, !next)
|
||||
conversationStore.setSilent(type, targetId, next)
|
||||
groupStore.setSilent(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationGroupSide] setSilent 失败', { targetId }, error)
|
||||
conversationStore.setSilent(type, targetId, !next)
|
||||
message.error('操作失败')
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
:clickable="false"
|
||||
/>
|
||||
<span
|
||||
v-show="!conversation.muted && conversation.unreadCount > 0"
|
||||
v-show="!conversation.silent && 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 }}
|
||||
|
|
@ -58,10 +58,10 @@
|
|||
</span>
|
||||
<!-- 免打扰图标 -->
|
||||
<Icon
|
||||
v-if="conversation.muted"
|
||||
v-if="conversation.silent"
|
||||
icon="mdi:bell-off-outline"
|
||||
:size="14"
|
||||
class="conversation-item__muted flex-shrink-0 ml-1 text-[var(--el-text-color-disabled)]"
|
||||
class="conversation-item__silent flex-shrink-0 ml-1 text-[var(--el-text-color-disabled)]"
|
||||
title="消息免打扰"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -192,17 +192,17 @@ function handleTop() {
|
|||
|
||||
/** 切换免打扰:乐观 UI(先本地切换,菜单立即关;后端失败回滚 conversation 状态) */
|
||||
function handleMuted() {
|
||||
const next = !props.conversation.muted
|
||||
const next = !props.conversation.silent
|
||||
const { type, targetId } = props.conversation
|
||||
conversationStore.setMuted(type, targetId, next)
|
||||
conversationStore.setSilent(type, targetId, next)
|
||||
const sync =
|
||||
type === ImConversationType.PRIVATE
|
||||
? friendStore.setMuted(targetId, next)
|
||||
: groupStore.setMuted(targetId, next)
|
||||
? friendStore.setSilent(targetId, next)
|
||||
: groupStore.setSilent(targetId, next)
|
||||
sync.catch((e) => {
|
||||
console.error('[IM] 切换免打扰失败', e)
|
||||
message.error('切换免打扰失败')
|
||||
conversationStore.setMuted(type, targetId, !next)
|
||||
conversationStore.setSilent(type, targetId, !next)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ function handleContextMenu(e: MouseEvent) {
|
|||
{ x: e.clientX, y: e.clientY },
|
||||
[
|
||||
{ key: 'TOP', name: props.conversation.top ? '取消置顶' : '置顶' },
|
||||
{ key: 'MUTED', name: props.conversation.muted ? '允许消息通知' : '消息免打扰' },
|
||||
{ key: 'MUTED', name: props.conversation.silent ? '允许消息通知' : '消息免打扰' },
|
||||
{ key: 'DELETE', name: '删除', divided: true, danger: true }
|
||||
],
|
||||
(item) => {
|
||||
|
|
@ -276,7 +276,7 @@ function formatTime(timestamp: number): string {
|
|||
}
|
||||
|
||||
/* el-icon 的全局 color:var(--color) 在暗色模式下会渲染成白色,这里用 :deep + !important 锁定 */
|
||||
.conversation-item__muted :deep(svg) {
|
||||
.conversation-item__silent :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@
|
|||
<div class="im-conversation-private-side__section">
|
||||
<div class="im-conversation-private-side__row">
|
||||
<span class="im-conversation-private-side__label">消息免打扰</span>
|
||||
<el-switch :model-value="!!conversation?.muted" @change="handleMutedChange" />
|
||||
<el-switch :model-value="!!conversation?.silent" @change="handleMutedChange" />
|
||||
</div>
|
||||
<div class="im-conversation-private-side__row">
|
||||
<span class="im-conversation-private-side__label">置顶聊天</span>
|
||||
|
|
@ -217,14 +217,14 @@ function handleMutedChange(value: boolean | string | number) {
|
|||
}
|
||||
const next = !!value
|
||||
const { type, targetId } = props.conversation
|
||||
conversationStore.setMuted(type, targetId, next)
|
||||
conversationStore.setSilent(type, targetId, next)
|
||||
if (type !== ImConversationType.PRIVATE) {
|
||||
return
|
||||
}
|
||||
friendStore.setMuted(targetId, next).catch((error) => {
|
||||
friendStore.setSilent(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, error)
|
||||
message.error('切换免打扰失败')
|
||||
conversationStore.setMuted(type, targetId, !next)
|
||||
conversationStore.setSilent(type, targetId, !next)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -247,7 +247,7 @@ function handleGroupCreated(groupId: number) {
|
|||
ImConversationType.GROUP,
|
||||
group.name,
|
||||
group.avatar || '',
|
||||
{ muted: !!group.muted }
|
||||
{ silent: !!group.silent }
|
||||
)
|
||||
visible.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ const renderedPinnedConversations = computed(() =>
|
|||
|
||||
/** 与会话项右上角红点的可见条件保持一致:免打扰不亮,无未读不亮 */
|
||||
function hasUnreadBadge(conversation: Conversation): boolean {
|
||||
return !conversation.muted && (conversation.unreadCount || 0) > 0
|
||||
return !conversation.silent && (conversation.unreadCount || 0) > 0
|
||||
}
|
||||
|
||||
/** 是否为当前激活会话 */
|
||||
|
|
@ -227,7 +227,7 @@ function handleGroupCreated(groupId: number) {
|
|||
ImConversationType.GROUP,
|
||||
group.name,
|
||||
group.avatar || '',
|
||||
{ muted: !!group.muted }
|
||||
{ silent: !!group.silent }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
/** 未读总数(免打扰会话不计入)—— 用于 ToolBar 红点 */
|
||||
getTotalUnread(state): number {
|
||||
return state.conversations
|
||||
.filter((c) => !c.deleted && !c.muted)
|
||||
.filter((c) => !c.deleted && !c.silent)
|
||||
.reduce((sum, c) => sum + (c.unreadCount || 0), 0)
|
||||
},
|
||||
/** 查找会话:按 (type, targetId) 组合主键 */
|
||||
|
|
@ -169,7 +169,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
/**
|
||||
* 持久化到 IndexedDB(fire-and-forget;调用方无需 await)
|
||||
*
|
||||
* - 不传 target:仅写 meta(适用于 top / muted / unread 等元数据变更)
|
||||
* - 不传 target:仅写 meta(适用于 top / silent / unread 等元数据变更)
|
||||
* - 传单个 conversation:写 meta + 该会话的消息(单条消息变更走这里)
|
||||
* - 传数组:写 meta + 数组里所有未删除会话的消息(loading 完成后兜底 flush 用)
|
||||
*
|
||||
|
|
@ -229,7 +229,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
/**
|
||||
* 打开或创建一个会话,并设为激活
|
||||
*
|
||||
* 调用方应该把从 friendStore / groupStore 拿到的最新元数据(muted 等)
|
||||
* 调用方应该把从 friendStore / groupStore 拿到的最新元数据(silent 等)
|
||||
* 通过 options 传进来,避免新建/复用的会话显示陈旧状态。
|
||||
* 此处不在 conversationStore 里反向 import friendStore/groupStore,是为了避免循环依赖。
|
||||
*/
|
||||
|
|
@ -238,26 +238,26 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
type: number,
|
||||
name: string,
|
||||
avatar: string,
|
||||
options?: { muted?: boolean }
|
||||
options?: { silent?: boolean }
|
||||
): Conversation {
|
||||
// 按 (type, targetId) 查找已有会话,不存在则新建并插到列表头部
|
||||
let conversation = this.getConversation(type, targetId)
|
||||
if (!conversation) {
|
||||
conversation = this.createEmptyConversation(type, targetId, name, avatar)
|
||||
if (options?.muted !== undefined) {
|
||||
conversation.muted = options.muted
|
||||
if (options?.silent !== undefined) {
|
||||
conversation.silent = options.silent
|
||||
}
|
||||
this.conversations.unshift(conversation)
|
||||
} else {
|
||||
// 已存在会话:用最新元数据刷新 name / avatar / muted
|
||||
// 已存在会话:用最新元数据刷新 name / avatar / silent
|
||||
if (name) {
|
||||
conversation.name = name
|
||||
}
|
||||
if (avatar) {
|
||||
conversation.avatar = avatar
|
||||
}
|
||||
if (options?.muted !== undefined) {
|
||||
conversation.muted = options.muted
|
||||
if (options?.silent !== undefined) {
|
||||
conversation.silent = options.silent
|
||||
}
|
||||
}
|
||||
this.setActiveConversation(conversation)
|
||||
|
|
@ -294,7 +294,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
messages: [],
|
||||
deleted: false,
|
||||
top: false,
|
||||
muted: false,
|
||||
silent: false,
|
||||
atMe: false,
|
||||
atAll: false
|
||||
}
|
||||
|
|
@ -312,13 +312,13 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
this.saveConversations()
|
||||
},
|
||||
|
||||
/** 设置会话免打扰(本地状态;后端同步由 friendStore / groupStore + /muted API 负责) */
|
||||
setMuted(type: number, targetId: number, muted: boolean) {
|
||||
/** 设置会话免打扰(本地状态;后端同步由 friendStore / groupStore + /silent API 负责) */
|
||||
setSilent(type: number, targetId: number, silent: boolean) {
|
||||
const conversation = this.getConversation(type, targetId)
|
||||
if (!conversation) {
|
||||
return
|
||||
}
|
||||
conversation.muted = muted
|
||||
conversation.silent = silent
|
||||
this.saveConversations()
|
||||
},
|
||||
|
||||
|
|
@ -718,7 +718,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
},
|
||||
|
||||
/**
|
||||
* 同步会话的展示元数据(name / avatar / muted)
|
||||
* 同步会话的展示元数据(name / avatar / silent)
|
||||
*
|
||||
* 调用方负责把好友 / 群的信息整理成 Conversation 视角的字段:
|
||||
* - 私聊:name = friend.nickname;avatar = friend.avatar
|
||||
|
|
@ -727,7 +727,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
updateConversation(
|
||||
type: number,
|
||||
targetId: number,
|
||||
info: { name?: string; avatar?: string; muted?: boolean }
|
||||
info: { name?: string; avatar?: string; silent?: boolean }
|
||||
) {
|
||||
const conversation = this.getConversation(type, targetId)
|
||||
if (!conversation) {
|
||||
|
|
@ -742,8 +742,8 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
conversation.avatar = info.avatar || ''
|
||||
changed = true
|
||||
}
|
||||
if (info.muted !== undefined && conversation.muted !== info.muted) {
|
||||
conversation.muted = info.muted
|
||||
if (info.silent !== undefined && conversation.silent !== info.silent) {
|
||||
conversation.silent = info.silent
|
||||
changed = true
|
||||
}
|
||||
if (changed) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export interface FriendNotificationPayload {
|
|||
fromAvatar?: string
|
||||
// FRIEND_UPDATE:单边属性变更
|
||||
displayName?: string
|
||||
muted?: boolean
|
||||
silent?: boolean
|
||||
pinned?: boolean
|
||||
// FRIEND_DELETE:是否级联清理本端相关数据(如私聊会话)
|
||||
clear?: boolean
|
||||
|
|
@ -164,7 +164,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
|
||||
name: getFriendDisplayName(friend),
|
||||
avatar: friend.avatar,
|
||||
muted: friend.muted
|
||||
silent: friend.silent
|
||||
})
|
||||
}
|
||||
this.saveFriends()
|
||||
|
|
@ -303,14 +303,14 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
this.removeFriend(friendUserId, clear)
|
||||
},
|
||||
|
||||
/** 切换免打扰:同步会话的 muted 字段,避免会话列表 muted 图标等 1210 推到才更新 */
|
||||
async setMuted(friendUserId: number, muted: boolean) {
|
||||
await apiUpdateFriend({ friendUserId, muted })
|
||||
/** 切换免打扰:同步会话的 silent 字段,避免会话列表 silent 图标等 1210 推到才更新 */
|
||||
async setSilent(friendUserId: number, silent: boolean) {
|
||||
await apiUpdateFriend({ friendUserId, silent })
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
friend.muted = muted
|
||||
friend.silent = silent
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, { muted })
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, { silent })
|
||||
this.saveFriends()
|
||||
}
|
||||
},
|
||||
|
|
@ -381,7 +381,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
|
||||
name: merged ? getFriendDisplayName(merged) : friend.nickname,
|
||||
avatar: friend.avatar,
|
||||
muted: friend.muted
|
||||
silent: friend.silent
|
||||
})
|
||||
this.saveFriends()
|
||||
},
|
||||
|
|
@ -479,8 +479,8 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
if (payload.displayName != null) {
|
||||
friend.displayName = payload.displayName
|
||||
}
|
||||
if (payload.muted != null) {
|
||||
friend.muted = payload.muted
|
||||
if (payload.silent != null) {
|
||||
friend.silent = payload.silent
|
||||
}
|
||||
if (payload.pinned != null) {
|
||||
friend.pinned = payload.pinned
|
||||
|
|
@ -488,7 +488,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, payload.friendUserId, {
|
||||
name: getFriendDisplayName(friend),
|
||||
muted: friend.muted
|
||||
silent: friend.silent
|
||||
})
|
||||
this.saveFriends()
|
||||
},
|
||||
|
|
@ -510,7 +510,7 @@ function convertFriend(vo: ImFriendRespVO): Friend {
|
|||
nickname: vo.nickname || String(vo.friendUserId),
|
||||
nicknamePinyin: vo.nicknamePinyin,
|
||||
avatar: vo.avatar,
|
||||
muted: !!vo.muted,
|
||||
silent: !!vo.silent,
|
||||
displayName: vo.displayName || '',
|
||||
displayNamePinyin: vo.displayNamePinyin,
|
||||
addSource: vo.addSource,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const pendingMemberKey = (userId: number, groupId: number) => `${userId}:${group
|
|||
/**
|
||||
* fetchGroupMember 单成员并发去重表:同 (groupId, memberUserId) 同时进的请求共用一个 Promise
|
||||
*
|
||||
* 跟整群表分开:单成员 fetch 跟整群 fetch 语义不同(单成员不回填 me 的 muted),不能互相代替
|
||||
* 跟整群表分开:单成员 fetch 跟整群 fetch 语义不同(单成员不回填 me 的 silent),不能互相代替
|
||||
*/
|
||||
const pendingSingleMemberFetches = new Map<string, Promise<GroupMember | null>>()
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 fetchGroupMembers)
|
||||
const list = await apiGetMyGroupList()
|
||||
const fresh = (list || []).map(convertGroup)
|
||||
// 合并而非全量替换:muted / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留
|
||||
// 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留
|
||||
const groupMap = new Map(this.groups.map((group) => [group.id, group]))
|
||||
this.groups = fresh.map((group) => {
|
||||
const existing = groupMap.get(group.id)
|
||||
|
|
@ -188,7 +188,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
...group,
|
||||
members: existing.members,
|
||||
memberCount: existing.memberCount ?? group.memberCount,
|
||||
muted: existing.muted ?? group.muted,
|
||||
silent: existing.silent ?? group.silent,
|
||||
groupRemark: existing.groupRemark,
|
||||
membersLoaded: existing.membersLoaded
|
||||
}
|
||||
|
|
@ -199,7 +199,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
|
||||
name: getGroupDisplayName(group),
|
||||
avatar: group.avatar,
|
||||
muted: group.muted
|
||||
silent: group.silent
|
||||
})
|
||||
}
|
||||
this.saveGroups()
|
||||
|
|
@ -237,7 +237,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
return inflight
|
||||
}
|
||||
const promise = (async () => {
|
||||
// 拉接口 + 单 pass 转换:同时捕获 me 的原始 VO,给下面回填 user-per-group 字段(muted / groupRemark)用
|
||||
// 拉接口 + 单 pass 转换:同时捕获 me 的原始 VO,给下面回填 user-per-group 字段(silent / groupRemark)用
|
||||
const list = await apiGetGroupMemberList(groupId)
|
||||
let meRaw: ImGroupMemberRespVO | undefined
|
||||
const members = (list || []).map((member) => {
|
||||
|
|
@ -246,7 +246,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
return convertGroupMember(member, groupId)
|
||||
})
|
||||
const muted = !!meRaw?.muted
|
||||
const silent = !!meRaw?.silent
|
||||
const groupRemark = meRaw?.groupRemark || ''
|
||||
|
||||
// 必须 await 之后重新 getGroup,避免 fetchGroups 已并发写入真实 group 的 race
|
||||
|
|
@ -261,7 +261,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
name: '',
|
||||
members,
|
||||
memberCount: members.length,
|
||||
muted,
|
||||
silent,
|
||||
groupRemark,
|
||||
membersLoaded: true
|
||||
})
|
||||
|
|
@ -269,15 +269,15 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
group.members = members
|
||||
group.memberCount = members.length
|
||||
group.membersLoaded = true
|
||||
// muted / groupRemark 任一变化才同步到 conversation 和 IDB;groupRemark 变化要顺带刷会话名
|
||||
if (group.muted !== muted || group.groupRemark !== groupRemark) {
|
||||
group.muted = muted
|
||||
// silent / groupRemark 任一变化才同步到 conversation 和 IDB;groupRemark 变化要顺带刷会话名
|
||||
if (group.silent !== silent || group.groupRemark !== groupRemark) {
|
||||
group.silent = silent
|
||||
group.groupRemark = groupRemark
|
||||
groupFieldsChanged = true
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
|
||||
name: getGroupDisplayName(group),
|
||||
muted
|
||||
silent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -299,7 +299,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
/**
|
||||
* 按 (groupId, memberUserId) 单成员补齐——deriveLastSenderDisplayName 兜底场景用
|
||||
*
|
||||
* 跟 fetchGroupMembers 区别:只拉这一个成员,不动 me 的 muted / groupRemark(不是 me 的话拿不到);
|
||||
* 跟 fetchGroupMembers 区别:只拉这一个成员,不动 me 的 silent / groupRemark(不是 me 的话拿不到);
|
||||
* 命中时把成员 upsert 进 group.members 数组并落 IDB,让后续渲染能用 displayUserName
|
||||
*/
|
||||
fetchGroupMember(groupId: number, memberUserId: number): Promise<GroupMember | null> {
|
||||
|
|
@ -366,7 +366,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
|
||||
name: getGroupDisplayName(merged),
|
||||
avatar: merged.avatar,
|
||||
muted: merged.muted
|
||||
silent: merged.silent
|
||||
})
|
||||
// 持久化到 IDB(fire-and-forget)
|
||||
this.saveGroups()
|
||||
|
|
@ -390,13 +390,13 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
},
|
||||
|
||||
/** 切换免打扰:推后端 + 落本地 */
|
||||
async setMuted(id: number, muted: boolean) {
|
||||
await apiUpdateGroupMember({ groupId: id, muted })
|
||||
async setSilent(id: number, silent: boolean) {
|
||||
await apiUpdateGroupMember({ groupId: id, silent })
|
||||
const group = this.getGroup(id)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
group.muted = muted
|
||||
group.silent = silent
|
||||
this.saveGroups()
|
||||
},
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
|
||||
name: getGroupDisplayName(group),
|
||||
avatar: group.avatar,
|
||||
muted: group.muted
|
||||
silent: group.silent
|
||||
})
|
||||
this.saveGroups()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
apiReadPrivateMessages(peerId, websocketMessage.id).catch((e) => {
|
||||
console.warn('[IM WS] 自动已读上报失败', e)
|
||||
})
|
||||
} else if (!conversation?.muted && isNormalMessage(websocketMessage.type)) {
|
||||
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
||||
// 非当前会话且未免打扰:响一下提示音(带节流,详见 playAudioTip);TIP_TEXT 等系统提示不响
|
||||
playAudioTip()
|
||||
}
|
||||
|
|
@ -439,7 +439,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
apiReadGroupMessages(websocketMessage.groupId, websocketMessage.id).catch((e) => {
|
||||
console.warn('[IM WS] 自动已读上报失败', e)
|
||||
})
|
||||
} else if (!conversation?.muted && isNormalMessage(websocketMessage.type)) {
|
||||
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
||||
// GROUP_* 群广播事件 / TIP_TEXT 等系统提示不响提示音
|
||||
playAudioTip()
|
||||
}
|
||||
|
|
@ -521,13 +521,13 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
// ==================== 群关系事件(承载于群聊通道,按 inner type 分流) ====================
|
||||
|
||||
/**
|
||||
* GROUP_MEMBER_SETTING_UPDATE:多端同步成员个人设置变更(muted / groupRemark)
|
||||
* GROUP_MEMBER_SETTING_UPDATE:多端同步成员个人设置变更(silent / groupRemark)
|
||||
*
|
||||
* payload 携带变更字段,按非 null 字段直接局部更新;省一次 fetchGroupMembers 接口
|
||||
*/
|
||||
handleGroupMemberSettingUpdate(websocketMessage: ImGroupMessageDTO) {
|
||||
// content 解析失败由外层 dispatchGroupFrame 的 try-catch 兜底(含 websocketMessage 打印),不重复 catch
|
||||
const payload: { muted?: boolean; groupRemark?: string } = JSON.parse(
|
||||
const payload: { silent?: boolean; groupRemark?: string } = JSON.parse(
|
||||
websocketMessage.content || '{}'
|
||||
)
|
||||
const groupStore = useGroupStore()
|
||||
|
|
@ -536,8 +536,8 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
return
|
||||
}
|
||||
const fields: Partial<Group> = {}
|
||||
if (payload.muted != null) {
|
||||
fields.muted = payload.muted
|
||||
if (payload.silent != null) {
|
||||
fields.silent = payload.silent
|
||||
}
|
||||
if (payload.groupRemark != null) {
|
||||
fields.groupRemark = payload.groupRemark
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export interface Conversation {
|
|||
// ========== UI 状态 ==========
|
||||
deleted?: boolean // 是否已删除(软删标记,持久化时过滤)
|
||||
top?: boolean // 是否置顶(排序时优先)
|
||||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
silent?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
atMe?: boolean // 群聊:是否有人 @我
|
||||
atAll?: boolean // 群聊:是否有人 @全体成员
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ export interface Group {
|
|||
pinnedMessages?: Message[] // 群置顶消息列表
|
||||
|
||||
// ========== 前端扩展字段(user-per-group 维度) ==========
|
||||
muted?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
||||
silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
||||
groupRemark?: string // 群备注。从当前用户的 GroupMember 回填(当前用户对该群的自定义名)
|
||||
members?: GroupMember[] // 群成员缓存(按需懒加载)
|
||||
membersLoaded?: boolean // members 是否"完整加载"——只有整群 loadGroupMembers / fetchGroupMembers 命中时为 true;fetchGroupMember 单成员补齐不置位,避免 fetchGroupMembers(force=false) 命中缓存时误判整群已加载
|
||||
|
|
@ -148,7 +148,7 @@ export interface Friend {
|
|||
nickname: string // 好友昵称(对方真实昵称,永远不被备注覆盖;UI 显示走 displayName || nickname)
|
||||
nicknamePinyin?: string // 昵称的拼音(后端用 Pinyin4j 算好回填,小写无空格)
|
||||
avatar?: string // 好友头像
|
||||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
silent?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
displayName?: string // 好友展示备注:仅自己可见的别名(单字段不歧义,不带 Friend 前缀)
|
||||
displayNamePinyin?: string // 备注的拼音(后端用 Pinyin4j 算好回填,小写无空格)
|
||||
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除,软删保留记录)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="免打扰" prop="muted">
|
||||
<el-form-item label="免打扰" prop="silent">
|
||||
<el-select
|
||||
v-model="queryParams.muted"
|
||||
v-model="queryParams.silent"
|
||||
placeholder="请选择免打扰状态"
|
||||
clearable
|
||||
class="!w-160px"
|
||||
|
|
@ -79,9 +79,9 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="displayName" width="120" />
|
||||
<el-table-column label="免打扰" align="center" prop="muted" width="80">
|
||||
<el-table-column label="免打扰" align="center" prop="silent" width="80">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.muted" />
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.silent" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
|
|
@ -131,7 +131,7 @@ const queryParams = reactive({
|
|||
userId: undefined as number | undefined,
|
||||
friendUserId: undefined as number | undefined,
|
||||
status: undefined as number | undefined,
|
||||
muted: undefined as boolean | undefined,
|
||||
silent: undefined as boolean | undefined,
|
||||
addTime: [] as string[]
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@
|
|||
>
|
||||
<template #default="{ row }">{{ row.groupRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="免打扰" prop="muted" width="80" align="center">
|
||||
<el-table-column label="免打扰" prop="silent" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.muted" />
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.silent" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" width="100" align="center">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const ImMessageType = {
|
|||
FRIEND_BLOCK: 1207, // 加入黑名单
|
||||
FRIEND_UNBLOCK: 1208, // 移出黑名单
|
||||
FRIEND_INFO_UPDATED: 1209, // 好友资料变更(昵称 / 头像)
|
||||
FRIEND_UPDATE: 1210, // 好友信息批量更新(muted / pinned)
|
||||
FRIEND_UPDATE: 1210, // 好友信息批量更新(silent / pinned)
|
||||
// ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 自有扩展段) ==========
|
||||
GROUP_CREATE: 1501, // 群创建
|
||||
GROUP_INFO_UPDATE: 1502, // 群信息变更(NAME / NOTICE 之外字段兜底)
|
||||
|
|
@ -42,7 +42,7 @@ export const ImMessageType = {
|
|||
GROUP_NOTICE_UPDATE: 1519, // 群公告变更
|
||||
GROUP_NAME_UPDATE: 1520, // 群名变更
|
||||
// ========== 自有扩展段(1530+,OpenIM 1500-1520 段位无对应物) ==========
|
||||
GROUP_MEMBER_SETTING_UPDATE: 1530, // 群成员个人设置变更:muted / groupRemark 个人多端同步
|
||||
GROUP_MEMBER_SETTING_UPDATE: 1530, // 群成员个人设置变更:silent / groupRemark 个人多端同步
|
||||
GROUP_MESSAGE_PIN: 1531, // 群消息置顶(自有扩展,OpenIM 无)
|
||||
GROUP_MESSAGE_UNPIN: 1532 // 群消息取消置顶(自有扩展,OpenIM 无)
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const StorageKeys = {
|
|||
/**
|
||||
* 会话索引:游标 + 会话元数据(不含 messages),对应 ConversationStoreMeta
|
||||
*
|
||||
* 任何会话级元数据变更(top / muted / unread / 列表增删 / 排序)都会重写这一个 key;由于 messages 已剥离到独立 key,单次写体积稳定(仅元数据,量级 KB 级)
|
||||
* 任何会话级元数据变更(top / silent / unread / 列表增删 / 排序)都会重写这一个 key;由于 messages 已剥离到独立 key,单次写体积稳定(仅元数据,量级 KB 级)
|
||||
*/
|
||||
conversationMeta: (userId: number | string) => `conversation:meta:${userId}`,
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue