9.16下午更新 包含im
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<!-- 处理滚动穿透 此为官方推荐做法 https://uniapp.dcloud.net.cn/component/uniui/uni-popup.html#%E4%BB%8B%E7%BB%8D -->
|
||||
<page-meta
|
||||
:page-style="'overflow:' + (moveThrough ? 'hidden' : 'visible')"
|
||||
></page-meta>
|
||||
<div>
|
||||
<NavBar
|
||||
:title="
|
||||
forwardConversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? t('teamChooseText')
|
||||
: t('chooseText')
|
||||
"
|
||||
:showLeft="true"
|
||||
>
|
||||
<template v-slot:left>
|
||||
<div @tap="backToChat">
|
||||
<Icon type="icon-zuojiantou" :size="22"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</NavBar>
|
||||
<div
|
||||
v-if="
|
||||
forwardConversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
"
|
||||
>
|
||||
<div class="group-list-content">
|
||||
<Empty v-if="teamList.length === 0" :text="t('TeamEmptyText')" />
|
||||
<div v-else>
|
||||
<div
|
||||
class="group-item"
|
||||
v-for="team in teamList"
|
||||
:key="team.teamId"
|
||||
@click="() => handleItemClick(team.teamId)"
|
||||
>
|
||||
<Avatar :account="team.teamId" :avatar="team.avatar" />
|
||||
<span class="group-name">{{ team.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="friendGroupList.length > 0" class="friend-list-container">
|
||||
<div class="friend-group-list">
|
||||
<div
|
||||
class="friend-group-item"
|
||||
v-for="friendGroup in friendGroupList"
|
||||
:key="friendGroup.key"
|
||||
>
|
||||
<div class="friend-group-title">
|
||||
{{ friendGroup.key }}
|
||||
</div>
|
||||
<div
|
||||
class="friend-item"
|
||||
v-for="friend in friendGroup.data"
|
||||
:key="friend.account"
|
||||
@click="() => handleItemClick(friend.account)"
|
||||
>
|
||||
<Avatar :account="friend.account" size="36" />
|
||||
<div class="friend-name">{{ friend.appellation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Empty v-else :text="t('noFriendText')" />
|
||||
</div>
|
||||
<!-- 转发弹窗 -->
|
||||
<ForwardModal
|
||||
:forward-modal-visible="forwardModalVisible"
|
||||
:forward-to="forwardTo"
|
||||
:forward-msg="forwardMsg"
|
||||
:forward-conversation-type="forwardConversationType"
|
||||
:forward-to-team-info="forwardToTeamInfo"
|
||||
@confirm="handleForwardConfirm"
|
||||
@cancel="handleForwardCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**消息转发页面 */
|
||||
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import { friendGroupByPy } from '@/utils/im/friend'
|
||||
import { autorun } from 'mobx'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import ForwardModal from './message/message-forward-modal.vue'
|
||||
import { V2NIMConst } from 'nim-web-sdk-ng/dist/esm/nim'
|
||||
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMTeam } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
|
||||
|
||||
/** 好友列表 */
|
||||
const friendGroupList = ref<
|
||||
{ key: string; data: { account: string; appellation: string }[] }[]
|
||||
>([])
|
||||
|
||||
/** 转发类型 */
|
||||
const forwardConversationType = ref<V2NIMConst.V2NIMConversationType>(
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
|
||||
)
|
||||
|
||||
/** 群列表 */
|
||||
const teamList = ref<V2NIMTeam[]>([])
|
||||
|
||||
/** 会话ID */
|
||||
const conversationId = uni.$UIKitStore?.uiStore.selectedConversation
|
||||
|
||||
/** 转发消息idClient*/
|
||||
let msgIdClient = ''
|
||||
|
||||
/** 转发消息来源 */
|
||||
let origin = ''
|
||||
|
||||
/** 转发相关 */
|
||||
const forwardModalVisible = ref(false)
|
||||
/** 转发到 */
|
||||
const forwardTo = ref('')
|
||||
/** 转发消息内容 */
|
||||
const forwardMsg = ref<V2NIMMessageForUI>()
|
||||
/** 转发到的群信息 */
|
||||
const forwardToTeamInfo = ref<V2NIMTeam>()
|
||||
|
||||
const moveThrough = computed(() => {
|
||||
return forwardModalVisible.value
|
||||
})
|
||||
|
||||
/**转发消息确认 */
|
||||
const handleForwardConfirm = (forwardComment: string) => {
|
||||
forwardModalVisible.value = false
|
||||
|
||||
if (!forwardMsg.value) {
|
||||
uni.showToast({
|
||||
title: t('getForwardMessageFailed'),
|
||||
icon: 'error',
|
||||
})
|
||||
setTimeout(() => {
|
||||
backToChat()
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
|
||||
const forwardConversationId = uni.$UIKitNIM.V2NIMConversationIdUtil[
|
||||
forwardConversationType.value ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
|
||||
? 'p2pConversationId'
|
||||
: 'teamConversationId'
|
||||
](forwardTo.value)
|
||||
|
||||
uni.$UIKitStore.msgStore
|
||||
.forwardMsgActive(forwardMsg.value, forwardConversationId, forwardComment)
|
||||
.then(() => {
|
||||
uni.showToast({
|
||||
title: t('forwardSuccessText'),
|
||||
icon: 'none',
|
||||
duration: 1000,
|
||||
})
|
||||
setTimeout(() => {
|
||||
backToChat()
|
||||
}, 1000)
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({
|
||||
title: t('forwardFailedText'),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 取消转发弹窗
|
||||
*/
|
||||
const handleForwardCancel = () => {
|
||||
forwardModalVisible.value = false
|
||||
}
|
||||
|
||||
onLoad((props) => {
|
||||
forwardConversationType.value = Number(props?.forwardConversationType)
|
||||
msgIdClient = props?.msgIdClient
|
||||
origin = props?.origin
|
||||
})
|
||||
|
||||
/**群监听 */
|
||||
const teamListWatch = autorun(() => {
|
||||
teamList.value = uni.$UIKitStore.uiStore.teamList
|
||||
})
|
||||
|
||||
/**好友监听 */
|
||||
const friendsWatch = autorun(() => {
|
||||
const friendsWithoutBlacklist = uni.$UIKitStore.uiStore.friends
|
||||
.filter(
|
||||
(item) =>
|
||||
!uni.$UIKitStore.relationStore.blacklist.includes(item.accountId)
|
||||
)
|
||||
.map((item) => ({
|
||||
account: item.accountId,
|
||||
appellation: uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: item.accountId,
|
||||
teamId: forwardTo.value,
|
||||
}),
|
||||
}))
|
||||
|
||||
friendGroupList.value = friendGroupByPy(
|
||||
friendsWithoutBlacklist,
|
||||
{
|
||||
firstKey: 'appellation',
|
||||
},
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
/**回到聊天 */
|
||||
const backToChat = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/**点击转发选择列表 */
|
||||
const handleItemClick = (_forwardTo: string) => {
|
||||
if (_forwardTo && msgIdClient) {
|
||||
forwardTo.value = _forwardTo
|
||||
forwardMsg.value = uni.$UIKitStore.msgStore.getMsg(conversationId, [
|
||||
msgIdClient,
|
||||
])?.[0]
|
||||
|
||||
if (origin === 'pin') {
|
||||
const curPinMsgsMap = uni.$UIKitStore.msgStore.pinMsgs.get(conversationId)
|
||||
//@ts-ignore
|
||||
const pinInfo = [...curPinMsgsMap.values()].find((pinInfo) => {
|
||||
if (pinInfo.message) {
|
||||
return pinInfo.message.messageClientId === msgIdClient
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (pinInfo) {
|
||||
forwardMsg.value = pinInfo.message
|
||||
}
|
||||
} else if (origin === 'collection') {
|
||||
const msg = uni.$UIKitStore.msgStore.collectionMsgs.get(msgIdClient)
|
||||
if (msg) {
|
||||
forwardMsg.value = msg
|
||||
}
|
||||
}
|
||||
|
||||
forwardModalVisible.value = true
|
||||
if (
|
||||
forwardConversationType.value ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
) {
|
||||
forwardToTeamInfo.value = uni.$UIKitStore.teamStore.teams.get(_forwardTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
teamListWatch()
|
||||
friendsWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/common.scss';
|
||||
.nav-bar-text {
|
||||
color: #337eff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.group-list-content {
|
||||
height: calc(100% - 60px - var(--status-bar-height));
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
padding-right: 20px;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
|
||||
.friend-group-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.friend-group-title {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
color: #b3b7bc;
|
||||
border-bottom: 1rpx solid #e1e6e8;
|
||||
}
|
||||
|
||||
.friend-item {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.friend-name {
|
||||
margin-left: 12px;
|
||||
padding-right: 20px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,683 @@
|
||||
<template>
|
||||
<!-- 处理滚动穿透 此为官方推荐做法 https://uniapp.dcloud.net.cn/component/uniui/uni-popup.html#%E4%BB%8B%E7%BB%8D -->
|
||||
<page-meta
|
||||
:page-style="'overflow:' + (moveThrough ? 'hidden' : 'visible')"
|
||||
></page-meta>
|
||||
<div :class="isH5 ? 'msg-page-wrapper-h5' : 'msg-page-wrapper'">
|
||||
<navBar :title="title"></navBar>
|
||||
<!-- <NavBar :title="title" :subTitle="subTitle" :showLeft="true">
|
||||
<template v-slot:left>
|
||||
<div @click="backToConversation">
|
||||
<Icon type="icon-zuojiantou" :size="22"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</NavBar> -->
|
||||
<div class="msg-alert">
|
||||
<NetworkAlert />
|
||||
</div>
|
||||
<div :class="isH5 ? 'msg-wrapper-h5' : 'msg-wrapper'">
|
||||
<MessageList
|
||||
:conversationType="conversationType"
|
||||
:to="to"
|
||||
:msgs="msgs"
|
||||
:loading-more="loadingMore"
|
||||
:no-more="noMore"
|
||||
:reply-msgs-map="replyMsgsMap"
|
||||
/>
|
||||
</div>
|
||||
<div style="height: 'auto'">
|
||||
<MessageInput
|
||||
:reply-msgs-map="replyMsgsMap"
|
||||
:conversation-type="conversationType"
|
||||
:to="to"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import { trackInit } from '@/utils/im/reporter'
|
||||
import { autorun } from 'mobx'
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { getUniPlatform } from '@/utils/im/index'
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import { customSwitchTab } from '@/utils/im/customNavigate'
|
||||
import NetworkAlert from '@/components/NetworkAlert.vue'
|
||||
//import NavBar from './message/nav-bar.vue'
|
||||
import navBar from "@/components/navBar/navBar.vue"
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import MessageList from './message/message-list.vue'
|
||||
import MessageInput from './message/message-input.vue'
|
||||
import { HISTORY_LIMIT } from '@/utils/im/constants'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import { V2NIMConversationType } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMConversationService'
|
||||
import { V2NIMMessageRefer } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
|
||||
|
||||
export interface YxReplyMsg {
|
||||
messageClientId: string
|
||||
scene: V2NIMConst.V2NIMConversationType
|
||||
from: string
|
||||
receiverId: string
|
||||
to: string
|
||||
idServer: string
|
||||
time: number
|
||||
}
|
||||
const fromPage=ref('')
|
||||
trackInit('ChatUIKit')
|
||||
|
||||
const title = ref('')
|
||||
|
||||
const subTitle = ref('')
|
||||
|
||||
/**会话ID */
|
||||
const conversationId = uni.$UIKitStore.uiStore.selectedConversation
|
||||
/**会话类型 */
|
||||
const conversationType =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
|
||||
conversationId
|
||||
) as unknown as V2NIMConversationType
|
||||
|
||||
/**对话方 */
|
||||
const to =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
|
||||
conversationId
|
||||
)
|
||||
|
||||
const isH5 = getUniPlatform() === 'web'
|
||||
|
||||
/**处理uni-popup 引起的滚动穿透 */
|
||||
const moveThrough = ref(false)
|
||||
|
||||
/**回到会话列表 */
|
||||
const backToConversation = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/**读取是否需要显示群组消息已读未读的全局配置,默认 false */
|
||||
const teamManagerVisible = uni.$UIKitStore.localOptions.teamMsgReceiptVisible
|
||||
|
||||
/**读取是否需要显示 p2p 消息、p2p会话列表消息已读未读的全局配置,默认 false */
|
||||
const p2pMsgReceiptVisible = uni.$UIKitStore.localOptions.p2pMsgReceiptVisible
|
||||
|
||||
/** 读取是否需要显示在线离线的全局配置,默认true*/
|
||||
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
|
||||
|
||||
let isMounted = false
|
||||
|
||||
const loadingMore = ref(false)
|
||||
|
||||
/**是否还有更多历史消息 */
|
||||
|
||||
const noMore = ref(false)
|
||||
|
||||
/**消息列表 */
|
||||
const msgs = ref<V2NIMMessage[]>([])
|
||||
|
||||
/**回复消息map,用于回复消息的解析处理 */
|
||||
const replyMsgsMap = ref<Record<string, V2NIMMessage>>()
|
||||
|
||||
/** 解散群组回调 */
|
||||
const onTeamDismissed = (data: any) => {
|
||||
if (data.teamId === to) {
|
||||
uni.showModal({
|
||||
content: t('onDismissTeamText'),
|
||||
showCancel: false,
|
||||
success(data) {
|
||||
if (data.confirm) {
|
||||
backToConversation()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 自己主动离开群组或被管理员踢出回调 */
|
||||
const onTeamLeft = (data: any) => {
|
||||
uni
|
||||
.showToast({
|
||||
title: t('onRemoveTeamText'),
|
||||
icon: 'none',
|
||||
duration: 1000,
|
||||
})
|
||||
.then(() => {
|
||||
backToConversation()
|
||||
})
|
||||
}
|
||||
|
||||
/** 收到新消息 */
|
||||
const onReceiveMessages = (msgs: V2NIMMessage[]) => {
|
||||
const routes = getCurrentPages()
|
||||
const curRoute = routes[routes.length - 1].route
|
||||
|
||||
// 不是当前用户的其他端发送的消息且是当前会话的未读消息,才发送已读回执
|
||||
if (
|
||||
msgs.length &&
|
||||
!msgs[0]?.isSelf &&
|
||||
msgs[0].conversationId == conversationId &&
|
||||
curRoute?.includes('Chat/index')
|
||||
) {
|
||||
handleMsgReceipt(msgs)
|
||||
}
|
||||
uni.$emit(events.ON_SCROLL_BOTTOM, msgs)
|
||||
}
|
||||
|
||||
/** 处理收到消息的已读回执 */
|
||||
const handleMsgReceipt = (msg: V2NIMMessage[]) => {
|
||||
if (
|
||||
msg[0].conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
|
||||
p2pMsgReceiptVisible
|
||||
) {
|
||||
uni.$UIKitStore.msgStore.sendMsgReceiptActive(msg[0])
|
||||
} else if (
|
||||
msg[0].conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
|
||||
teamManagerVisible
|
||||
) {
|
||||
uni.$UIKitStore.msgStore.sendTeamMsgReceiptActive(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理历史消息的已读未读 */
|
||||
const handleHistoryMsgReceipt = (msgs: V2NIMMessage[]) => {
|
||||
/** 如果是单聊 */
|
||||
if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
|
||||
p2pMsgReceiptVisible
|
||||
) {
|
||||
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
|
||||
const othersMsgs = msgs
|
||||
.filter(
|
||||
(item: V2NIMMessage) =>
|
||||
// @ts-ignore
|
||||
!['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
|
||||
)
|
||||
.filter((item: V2NIMMessage) => item.senderId !== myUserAccountId)
|
||||
|
||||
/** 发送单聊消息已读回执 */
|
||||
if (othersMsgs.length > 0) {
|
||||
uni.$UIKitStore.msgStore.sendMsgReceiptActive(othersMsgs?.[0])
|
||||
}
|
||||
|
||||
/** 如果是群聊 */
|
||||
} else if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
|
||||
teamManagerVisible
|
||||
) {
|
||||
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
|
||||
const myMsgs = msgs
|
||||
.filter(
|
||||
(item: V2NIMMessage) =>
|
||||
// @ts-ignore
|
||||
!['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
|
||||
)
|
||||
.filter((item: V2NIMMessage) => item.senderId === myUserAccountId)
|
||||
|
||||
uni.$UIKitStore.msgStore.getTeamMsgReadsActive(myMsgs, conversationId)
|
||||
|
||||
// 发送群消息已读回执
|
||||
// sdk 要求 一次最多传入 50 个消息对象
|
||||
const othersMsgs = msgs
|
||||
.filter(
|
||||
(item: V2NIMMessage) =>
|
||||
// @ts-ignore
|
||||
!['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
|
||||
)
|
||||
.filter((item: V2NIMMessage) => item.senderId !== myUserAccountId)
|
||||
|
||||
if (othersMsgs.length > 0 && othersMsgs.length < 50) {
|
||||
uni.$UIKitStore.msgStore.sendTeamMsgReceiptActive(othersMsgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉取历史消息 */
|
||||
const getHistory = async (endTime: number, lastMsgId?: string) => {
|
||||
try {
|
||||
if (noMore.value) {
|
||||
return []
|
||||
}
|
||||
if (loadingMore.value) {
|
||||
return []
|
||||
}
|
||||
loadingMore.value = true
|
||||
if (conversationId) {
|
||||
const historyMsgs = await uni.$UIKitStore.msgStore.getHistoryMsgActive({
|
||||
conversationId,
|
||||
endTime,
|
||||
lastMsgId,
|
||||
limit: HISTORY_LIMIT,
|
||||
})
|
||||
// 在点击会话时,去获取并更新 pin 和 msg 信息。
|
||||
await uni.$UIKitStore.msgStore.getPinnedMessageListActive(conversationId)
|
||||
|
||||
loadingMore.value = false
|
||||
if (historyMsgs.length < HISTORY_LIMIT) {
|
||||
noMore.value = true
|
||||
}
|
||||
// 消息已读未读相关
|
||||
handleHistoryMsgReceipt(historyMsgs)
|
||||
return historyMsgs
|
||||
}
|
||||
} catch (error) {
|
||||
//@ts-ignore
|
||||
// 云端会话下,离线状态时,解散群聊,仍然可以拉到会话,但此时群已经解散
|
||||
if (error.code === 109404) {
|
||||
uni.showModal({
|
||||
content: t('onDismissTeamText'),
|
||||
showCancel: false,
|
||||
success(data) {
|
||||
if (data.confirm) {
|
||||
backToConversation()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
loadingMore.value = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载更多消息 */
|
||||
const loadMoreMsgs = (lastMsg: V2NIMMessage) => {
|
||||
if (lastMsg) {
|
||||
getHistory(lastMsg.createTime, lastMsg.messageServerId)
|
||||
} else {
|
||||
getHistory(Date.now())
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置页面标题 */
|
||||
const setNavTitle = () => {
|
||||
// 如果是单聊
|
||||
if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
|
||||
) {
|
||||
if (loginStateVisible) {
|
||||
subTitle.value =
|
||||
uni.$UIKitStore?.subscriptionStore.stateMap.get(to)?.statusType ===
|
||||
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
|
||||
? `(${t('userOnlineText')})`
|
||||
: `(${t('userOfflineText')})`
|
||||
}
|
||||
console.log('to:'+to);
|
||||
if(!fromPage.value){
|
||||
title.value = uni.$UIKitStore.uiStore.getAppellation({ account: to })
|
||||
}
|
||||
;
|
||||
|
||||
// 如果是群聊
|
||||
} else if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
) {
|
||||
const team = uni.$UIKitStore.teamStore.teams.get(to)
|
||||
subTitle.value = `(${team?.memberCount || 0})`
|
||||
|
||||
title.value = team?.name || ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听当前聊天页面的会话类型 */
|
||||
const conversationTypeWatch = autorun(() => {
|
||||
setNavTitle()
|
||||
})
|
||||
|
||||
/** 监听连接状态 */
|
||||
const connectedWatch = autorun(() => {
|
||||
if (
|
||||
uni.$UIKitStore.connectStore.connectStatus ===
|
||||
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
|
||||
) {
|
||||
if (
|
||||
uni.$UIKitStore.connectStore.loginStatus ==
|
||||
V2NIMConst.V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED
|
||||
) {
|
||||
getHistory(Date.now()).then(() => {
|
||||
if (!isMounted) {
|
||||
uni.$emit(events.ON_SCROLL_BOTTOM)
|
||||
isMounted = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** 处理回复消息 */
|
||||
const handleReplyMsg = (messages: V2NIMMessage[]) => {
|
||||
// 遍历所有消息,找出被回复消息,储存在map中
|
||||
if (messages.length !== 0) {
|
||||
const replyMsgsMapForExt: any = {}
|
||||
const replyMsgsMapForThreadReply: any = {}
|
||||
const extReqMsgs: YxReplyMsg[] = []
|
||||
const threadReplyReqMsgs: V2NIMMessageRefer[] = []
|
||||
const messageClientIds: Record<string, string> = {}
|
||||
msgs.value.forEach((msg) => {
|
||||
if (msg.serverExtension) {
|
||||
try {
|
||||
// yxReplyMsg 存储着被回复消息的相关消息
|
||||
const { yxReplyMsg } = JSON.parse(msg.serverExtension)
|
||||
if (yxReplyMsg) {
|
||||
// 从消息列表中找到被回复消息,replyMsg 为被回复的消息
|
||||
const replyMsg = msgs.value.find(
|
||||
(item) => item.messageClientId === yxReplyMsg.idClient
|
||||
)
|
||||
// 如果直接找到,存储在map中
|
||||
if (replyMsg) {
|
||||
replyMsgsMapForExt[msg.messageClientId] = replyMsg
|
||||
// 如果没找到,说明被回复的消息可能有三种情况:1.被删除 2.被撤回 3.不在当前消息列表中(一次性没拉到,在之前的消息中)
|
||||
} else {
|
||||
replyMsgsMapForExt[msg.messageClientId] = {
|
||||
messageClientId: 'noFind',
|
||||
}
|
||||
const {
|
||||
scene,
|
||||
from,
|
||||
to,
|
||||
idServer,
|
||||
messageClientId,
|
||||
time,
|
||||
receiverId,
|
||||
} = yxReplyMsg
|
||||
|
||||
if (
|
||||
scene &&
|
||||
from &&
|
||||
to &&
|
||||
idServer &&
|
||||
messageClientId &&
|
||||
time &&
|
||||
receiverId
|
||||
) {
|
||||
extReqMsgs.push({
|
||||
scene,
|
||||
from,
|
||||
to,
|
||||
idServer,
|
||||
messageClientId,
|
||||
time,
|
||||
receiverId,
|
||||
})
|
||||
messageClientIds[idServer] = msg.messageClientId
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (msg.threadReply) {
|
||||
//找到被回复的消息
|
||||
const beReplyMsg = msgs.value.find(
|
||||
(item) => item.messageClientId === msg.threadReply?.messageClientId
|
||||
)
|
||||
|
||||
if (beReplyMsg) {
|
||||
replyMsgsMapForThreadReply[msg.messageClientId] = beReplyMsg
|
||||
} else {
|
||||
replyMsgsMapForThreadReply[msg.messageClientId] = {
|
||||
messageClientId: 'noFind',
|
||||
}
|
||||
messageClientIds[msg.threadReply.messageServerId] =
|
||||
msg.messageClientId
|
||||
threadReplyReqMsgs.push(msg.threadReply)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (extReqMsgs.length > 0) {
|
||||
// 从服务器拉取被回复消息, 但是有频率控制
|
||||
uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers(
|
||||
//@ts-ignore
|
||||
extReqMsgs.map((item) => ({
|
||||
senderId: item.from,
|
||||
receiverId: item.receiverId,
|
||||
messageClientId: item.messageClientId,
|
||||
messageServerId: item.idServer,
|
||||
createTime: item.time,
|
||||
conversationType: item.scene,
|
||||
conversationId: item.to,
|
||||
}))
|
||||
)
|
||||
.then((res) => {
|
||||
if (res?.length > 0) {
|
||||
res.forEach((item) => {
|
||||
if (item.messageServerId) {
|
||||
replyMsgsMapForExt[messageClientIds[item.messageServerId]] =
|
||||
item
|
||||
}
|
||||
})
|
||||
}
|
||||
replyMsgsMap.value = { ...replyMsgsMapForExt }
|
||||
})
|
||||
.catch(() => {
|
||||
replyMsgsMap.value = { ...replyMsgsMapForExt }
|
||||
})
|
||||
}
|
||||
|
||||
replyMsgsMap.value = {
|
||||
...replyMsgsMap.value,
|
||||
...replyMsgsMapForThreadReply,
|
||||
}
|
||||
|
||||
if (threadReplyReqMsgs.length > 0) {
|
||||
uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers(
|
||||
//@ts-ignore
|
||||
threadReplyReqMsgs
|
||||
)
|
||||
.then((res) => {
|
||||
if (res?.length > 0) {
|
||||
res.forEach((item) => {
|
||||
if (item.messageServerId) {
|
||||
replyMsgsMapForThreadReply[
|
||||
messageClientIds[item.messageServerId]
|
||||
] = item
|
||||
}
|
||||
})
|
||||
}
|
||||
replyMsgsMap.value = {
|
||||
...replyMsgsMap.value,
|
||||
...replyMsgsMapForThreadReply,
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
replyMsgsMap.value = {
|
||||
...replyMsgsMap.value,
|
||||
...replyMsgsMapForThreadReply,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 动态更新消息 */
|
||||
const msgsWatch = autorun(() => {
|
||||
// 这里需要 Clone,否则 pinState 更新了,对应的消息展示不会重新渲染
|
||||
const messages = [...uni.$UIKitStore.msgStore.getMsg(conversationId)]
|
||||
if (messages.length !== 0) {
|
||||
msgs.value = messages
|
||||
}
|
||||
|
||||
// 处理回复消息
|
||||
handleReplyMsg(messages)
|
||||
|
||||
// 当聊天消息小于6条时,由于页面被键盘撑起,导致已经发出的消息不可见,所以需要隐藏键盘
|
||||
if (messages.length < 6) {
|
||||
uni.hideKeyboard()
|
||||
}
|
||||
})
|
||||
|
||||
/** 监听会话方在线离线状态 */
|
||||
const statusWatch = autorun(() => {
|
||||
const stateMap = uni.$UIKitStore?.subscriptionStore.stateMap
|
||||
if (
|
||||
uni.$UIKitStore.localOptions.loginStateVisible &&
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
|
||||
) {
|
||||
subTitle.value =
|
||||
stateMap.get(to)?.statusType ===
|
||||
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
|
||||
? `(${t('userOnlineText')})`
|
||||
: `(${t('userOfflineText')})`
|
||||
}
|
||||
})
|
||||
|
||||
/** 滚动到底部*/
|
||||
const scrollToBottom = () => {
|
||||
const timer = setTimeout(() => {
|
||||
uni.$emit(events.ON_SCROLL_BOTTOM)
|
||||
clearTimeout(timer)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 订阅在线离线状态 */
|
||||
const subscribeUserStatus = () => {
|
||||
if (
|
||||
loginStateVisible &&
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
|
||||
) {
|
||||
uni.$UIKitStore.subscriptionStore.subscribeUserStatusActive([to])
|
||||
}
|
||||
}
|
||||
|
||||
onShow(()=>{
|
||||
setNavTitle();
|
||||
//console.log(uni.$UIKitStore);
|
||||
console.log(3333);
|
||||
setTimeout(()=>{
|
||||
console.log(1111);
|
||||
uni.$UIKitStore?.userStore._getUserInfo(to).then(res=>{
|
||||
console.log(res)
|
||||
title.value=res.name;
|
||||
});
|
||||
console.log(22222);
|
||||
})
|
||||
// 从其他页面返回到聊天页时,可能使用的是 uni.navigateBack,此时不会触发onload等事件,但此时需要将收到的新消息发送已读未读
|
||||
if (msgs.value.length) {
|
||||
const _msgs = [...msgs.value].reverse()
|
||||
handleHistoryMsgReceipt(_msgs)
|
||||
}
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
fromPage.value=options.from;
|
||||
uni.$on(events.HANDLE_MOVE_THROUGH, (flag) => {
|
||||
moveThrough.value = flag
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setNavTitle()
|
||||
|
||||
scrollToBottom()
|
||||
|
||||
subscribeUserStatus()
|
||||
|
||||
/** 收到消息 */
|
||||
uni.$UIKitNIM.V2NIMMessageService.on(
|
||||
'onReceiveMessages',
|
||||
//@ts-ignore
|
||||
onReceiveMessages
|
||||
)
|
||||
/** 解散群组回调 */
|
||||
uni.$UIKitNIM.V2NIMTeamService.on('onTeamDismissed', onTeamDismissed)
|
||||
/** 自己主动离开群组或被管理员踢出回调 */
|
||||
uni.$UIKitNIM.V2NIMTeamService.on('onTeamLeft', onTeamLeft)
|
||||
/** 加载更多消息 */
|
||||
uni.$on(events.GET_HISTORY_MSG, loadMoreMsgs)
|
||||
})
|
||||
|
||||
//卸载相关事件监听
|
||||
onUnmounted(() => {
|
||||
uni.$UIKitNIM.V2NIMTeamService.off('onTeamDismissed', onTeamDismissed)
|
||||
uni.$UIKitNIM.V2NIMTeamService.off('onTeamLeft', onTeamLeft)
|
||||
uni.$UIKitNIM.V2NIMMessageService.off(
|
||||
'onReceiveMessages',
|
||||
//@ts-ignore
|
||||
onReceiveMessages
|
||||
)
|
||||
|
||||
uni.$off(events.GET_HISTORY_MSG, loadMoreMsgs)
|
||||
/** 移除store的数据监听 */
|
||||
connectedWatch()
|
||||
msgsWatch()
|
||||
statusWatch()
|
||||
conversationTypeWatch()
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
uni.hideKeyboard()
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off(events.CONFIRM_FORWARD_MSG)
|
||||
uni.$off(events.CANCEL_FORWARD_MSG)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
page {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msg-page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msg-page-wrapper-h5 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.msg-alert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msg-wrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.msg-wrapper-h5 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.msg-wrapper > message-list {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="msg-page-wrapper">
|
||||
<div class="msg-nav">
|
||||
<NavBar :title="t('msgReadPageTitleText')" :showLeft="true">
|
||||
<template v-slot:left>
|
||||
<div @click="backToConversation">
|
||||
<Icon type="icon-zuojiantou" :size="22"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
</NavBar>
|
||||
</div>
|
||||
<div class="msg-alert">
|
||||
<NetworkAlert />
|
||||
</div>
|
||||
<div class="msg-read-header">
|
||||
<div
|
||||
class="msg-read-header-item"
|
||||
:class="selectedType === 'read' ? 'active' : ''"
|
||||
@click="selectedType = 'read'"
|
||||
>
|
||||
{{ `${t('readText')}(${readCount})` }}
|
||||
</div>
|
||||
<div
|
||||
class="msg-read-header-item"
|
||||
:class="selectedType === 'unread' ? 'active' : ''"
|
||||
@click="selectedType = 'unread'"
|
||||
>
|
||||
{{ `${t('unreadText')}(${unReadCount})` }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="selectedType === 'read'" class="list-wrapper">
|
||||
<div
|
||||
v-if="readList.length"
|
||||
class="list-item"
|
||||
v-for="item in readList"
|
||||
:key="item"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
<Avatar
|
||||
size="40"
|
||||
:account="item"
|
||||
:goto-user-card="true"
|
||||
:teamId="teamId"
|
||||
:goto-team-card="false"
|
||||
/>
|
||||
</div>
|
||||
<Appellation :account="item" :teamId="teamId"></Appellation>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Empty :text="t('allUnReadText')"></Empty>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="selectedType === 'unread'" class="list-wrapper">
|
||||
<div
|
||||
v-if="unReadList.length"
|
||||
class="list-item"
|
||||
v-for="item in unReadList"
|
||||
:key="item"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
<Avatar
|
||||
size="40"
|
||||
:account="item"
|
||||
:goto-user-card="true"
|
||||
:teamId="teamId"
|
||||
:goto-team-card="false"
|
||||
/>
|
||||
</div>
|
||||
<Appellation :account="item" :teamId="teamId"></Appellation>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Empty :text="t('allReadText')"></Empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 消息已读未读详情页面 */
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import NetworkAlert from '@/components/NetworkAlert.vue'
|
||||
import NavBar from './message/nav-bar.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import Appellation from '@/components/Appellation.vue'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
|
||||
/** 已读人数 */
|
||||
const readCount = ref(0)
|
||||
/** 未读人数 */
|
||||
const unReadCount = ref(0)
|
||||
/** 已读列表 */
|
||||
const readList = ref<string[]>([])
|
||||
/** 未读列表 */
|
||||
const unReadList = ref<string[]>([])
|
||||
/** 已读未读类型 */
|
||||
const selectedType = ref<string>('read')
|
||||
/** 群ID */
|
||||
const teamId = ref<string>('')
|
||||
|
||||
/** 返回会话列表 */
|
||||
const backToConversation = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((props) => {
|
||||
const messageClientId = props?.messageClientId
|
||||
const conversationId = props?.conversationId
|
||||
if (messageClientId && conversationId) {
|
||||
teamId.value =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
|
||||
conversationId
|
||||
)
|
||||
const msg = uni.$UIKitStore.msgStore.getMsg(conversationId, [
|
||||
messageClientId,
|
||||
])
|
||||
if (msg.length) {
|
||||
// 获取当前消息的已读未读详情
|
||||
uni.$UIKitStore.msgStore
|
||||
.getTeamMessageReceiptDetailsActive(msg[0])
|
||||
.then((res) => {
|
||||
readCount.value = res?.readReceipt.readCount
|
||||
unReadCount.value = res?.readReceipt.unreadCount
|
||||
readList.value = res?.readAccountList
|
||||
setTimeout(() => {
|
||||
unReadList.value = res?.unreadAccountList
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.msg-nav {
|
||||
flex-basis: 45px;
|
||||
}
|
||||
.msg-read-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
.msg-read-header-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
}
|
||||
.active {
|
||||
border-bottom: 1px solid #007aff;
|
||||
}
|
||||
}
|
||||
.list-wrapper {
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.list-item {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.avatar-wrapper {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="msg-face-wrapper">
|
||||
<div class="msg-face">
|
||||
<div class="msg-face-row" v-for="(emojiRow, rowIndex) in emojiMatrix">
|
||||
<div
|
||||
@tap.stop="
|
||||
() => {
|
||||
handleEmojiClick({ key, type: emojiMap[key] })
|
||||
}
|
||||
"
|
||||
v-for="key in emojiRow"
|
||||
:key="key"
|
||||
class="msg-face-item"
|
||||
>
|
||||
<Icon :size="27" :type="emojiMap[key]"></Icon>
|
||||
</div>
|
||||
<!-- 下面放三个看不到的 Icon 占个位 -->
|
||||
<Icon
|
||||
v-if="rowIndex + 1 === Math.ceil(emojiArr.length / emojiColNum)"
|
||||
class="msg-face-delete"
|
||||
:size="27"
|
||||
type="icon-tuigejian"
|
||||
></Icon>
|
||||
<Icon
|
||||
v-if="rowIndex + 1 === Math.ceil(emojiArr.length / emojiColNum)"
|
||||
class="msg-face-delete"
|
||||
:size="27"
|
||||
type="icon-tuigejian"
|
||||
></Icon>
|
||||
<Icon
|
||||
v-if="rowIndex + 1 === Math.ceil(emojiArr.length / emojiColNum)"
|
||||
class="msg-face-delete"
|
||||
:size="27"
|
||||
type="icon-tuigejian"
|
||||
></Icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="emoji-block"></div>
|
||||
<div class="msg-face-control">
|
||||
<div @tap="handleEmojiDelete" class="msg-delete-btn">
|
||||
<Icon type="icon-tuigejian" :size="25" :color="'#333'" />
|
||||
</div>
|
||||
<div @tap="handleEmojiSend" class="msg-send-btn">{{ t('sendText') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 表情组件 */
|
||||
import { emojiMap } from '@/utils/im/emoji'
|
||||
import { calculateMatrix } from '@/utils/im/matrix'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
// 七个一行
|
||||
const emojiArr = Object.keys(emojiMap)
|
||||
const emojiColNum = 7
|
||||
// 处理表情需要网格布局
|
||||
const emojiMatrix = calculateMatrix(emojiArr, emojiColNum)
|
||||
|
||||
const emit = defineEmits(['emojiClick', 'emojiSend', 'emojiDelete'])
|
||||
|
||||
// 点击表情
|
||||
const handleEmojiClick = (emoji: any) => {
|
||||
emit('emojiClick', emoji)
|
||||
}
|
||||
|
||||
// 删除表情
|
||||
const handleEmojiDelete = () => {
|
||||
emit('emojiDelete')
|
||||
}
|
||||
|
||||
// 发送表情
|
||||
const handleEmojiSend = () => {
|
||||
emit('emojiSend')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-face-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.msg-face-control {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
right: 10px;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.emoji-block {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.msg-face {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 10px;
|
||||
// flex-wrap: wrap;
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 12px;
|
||||
|
||||
&:last-child {
|
||||
flex-basis: 57.14%;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
font-size: 27px;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
font-size: 27px;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-face-control {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg-send-btn {
|
||||
padding: 6px 16px;
|
||||
color: #fff;
|
||||
background-color: #337eff;
|
||||
}
|
||||
|
||||
.msg-delete-btn {
|
||||
background-color: #fff;
|
||||
margin-right: 10px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="mention-member-list-wrapper">
|
||||
<div class="header">
|
||||
<div @tap="onClosePopup" class="close">
|
||||
<Icon color="#000" type="icon-jiantou" />
|
||||
</div>
|
||||
<div class="title">{{ t('chooseMentionText') }}</div>
|
||||
</div>
|
||||
<div class="member-list-content">
|
||||
<div style="display: none">{{ teamExt }}</div>
|
||||
<div
|
||||
v-if="allowAtAll"
|
||||
class="member-item"
|
||||
@tap="
|
||||
() =>
|
||||
handleItemClick({
|
||||
accountId: AT_ALL_ACCOUNT,
|
||||
appellation: t('teamAll'),
|
||||
})
|
||||
"
|
||||
>
|
||||
<Icon :size="42" type="icon-team2" color="#fff" />
|
||||
<span class="member-name">
|
||||
{{ t('teamAll') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="member-item"
|
||||
v-for="member in teamMembersWithoutSelf"
|
||||
:key="member.accountId"
|
||||
@tap="() => handleItemClick(member)"
|
||||
>
|
||||
<Avatar :account="member.accountId" />
|
||||
<div class="member-name">
|
||||
<Appellation
|
||||
:account="member.accountId"
|
||||
:teamId="member.teamId"
|
||||
></Appellation>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
member.memberRole ===
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER
|
||||
"
|
||||
class="owner"
|
||||
>
|
||||
{{ t('teamOwner') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
member.memberRole ===
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
|
||||
"
|
||||
class="manager"
|
||||
>
|
||||
{{ t('teamManager') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-item-block"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**@ 列表组件,用于在群里@ 成员列表 */
|
||||
import { ref, computed, onUnmounted, withDefaults } from 'vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { autorun } from 'mobx'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { ALLOW_AT, AT_ALL_ACCOUNT } from '@/utils/im/constants'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import Appellation from '@/components/Appellation.vue'
|
||||
|
||||
import {
|
||||
V2NIMTeam,
|
||||
V2NIMTeamMember,
|
||||
} from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
|
||||
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import { MentionedMember } from './message-input.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
teamId: string
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
const team = ref<V2NIMTeam>()
|
||||
const teamMembers = ref<V2NIMTeamMember[]>([])
|
||||
const teamExt = ref('')
|
||||
|
||||
/** 群成员 不包括当前登录用户 */
|
||||
const teamMembersWithoutSelf = computed(() => {
|
||||
return teamMembers.value.filter(
|
||||
(item) => item.accountId !== uni.$UIKitStore.userStore.myUserInfo.accountId
|
||||
)
|
||||
})
|
||||
|
||||
/** 是否是群主 */
|
||||
const isGroupOwner = computed(() => {
|
||||
const myUser = uni.$UIKitStore.userStore.myUserInfo
|
||||
return (
|
||||
(team.value ? team.value.ownerAccountId : '') ===
|
||||
(myUser ? myUser.accountId : '')
|
||||
)
|
||||
})
|
||||
|
||||
/** 是否是群管理员 */
|
||||
const isGroupManager = computed(() => {
|
||||
const myUser = uni.$UIKitStore.userStore.myUserInfo
|
||||
return teamMembers.value
|
||||
.filter(
|
||||
(item) =>
|
||||
item.memberRole ===
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
|
||||
)
|
||||
.some((member) => member.accountId === (myUser ? myUser.accountId : ''))
|
||||
})
|
||||
|
||||
/** 是否允许@ 所有人 */
|
||||
const allowAtAll = computed(() => {
|
||||
let ext: any = {}
|
||||
try {
|
||||
ext = JSON.parse(teamExt.value || '{}')
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
if (ext[ALLOW_AT] === 'manager') {
|
||||
return isGroupOwner.value || isGroupManager.value
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
/** 群成员排序 群主 > 管理员 > 成员 */
|
||||
const sortGroupMembers = (members: V2NIMTeamMember[], teamId: string) => {
|
||||
const owner = members.filter(
|
||||
(item) =>
|
||||
item.memberRole ===
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER
|
||||
)
|
||||
const manager = members
|
||||
.filter(
|
||||
(item) =>
|
||||
item.memberRole ===
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
|
||||
)
|
||||
.sort((a, b) => a.joinTime - b.joinTime)
|
||||
const other = members
|
||||
.filter(
|
||||
(item) =>
|
||||
![
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER,
|
||||
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER,
|
||||
].includes(item.memberRole)
|
||||
)
|
||||
.sort((a, b) => a.joinTime - b.joinTime)
|
||||
const result = [...owner, ...manager, ...other].map((item) => {
|
||||
return {
|
||||
...item,
|
||||
|
||||
name: uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: item.accountId,
|
||||
teamId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 群成员点击函数
|
||||
*/
|
||||
const handleItemClick = (member: V2NIMTeamMember | MentionedMember) => {
|
||||
const _member: MentionedMember =
|
||||
member.accountId === AT_ALL_ACCOUNT
|
||||
? (member as MentionedMember)
|
||||
: {
|
||||
accountId: member.accountId,
|
||||
appellation: uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: member.accountId,
|
||||
teamId: (member as V2NIMTeamMember).teamId,
|
||||
ignoreAlias: true,
|
||||
}),
|
||||
}
|
||||
uni.$emit(events.HANDLE_AIT_MEMBER, _member)
|
||||
}
|
||||
|
||||
const onClosePopup = () => {
|
||||
uni.$emit(events.CLOSE_AIT_POPUP)
|
||||
}
|
||||
/** 监听群成员 */
|
||||
const teamMemberWatch = autorun(() => {
|
||||
if (props.teamId) {
|
||||
teamMembers.value = sortGroupMembers(
|
||||
//@ts-ignore
|
||||
uni.$UIKitStore.teamMemberStore.getTeamMember(props.teamId),
|
||||
props.teamId
|
||||
)
|
||||
// @ts-ignore
|
||||
const _team: V2NIMTeam = uni.$UIKitStore.teamStore.teams.get(props.teamId)
|
||||
if (team) {
|
||||
team.value = _team
|
||||
teamExt.value = _team?.serverExtension || ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
/** 移除监听 */
|
||||
teamMemberWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/common.scss';
|
||||
|
||||
.mention-member-list-wrapper {
|
||||
z-index: 9999999;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.close {
|
||||
transform: rotate(90deg);
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.member-list-content {
|
||||
height: 70vh;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.member-item-block {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
padding-right: 20px;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
|
||||
.contact-item-icon {
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 39px;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
background-color: #53c3f4;
|
||||
}
|
||||
|
||||
.owner,
|
||||
.manager {
|
||||
color: rgb(6, 155, 235);
|
||||
background-color: rgb(210, 229, 246);
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 2px 4px;
|
||||
position: relative;
|
||||
right: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div
|
||||
:class="!msg.isSelf || mode === 'audio-in' ? 'audio-in' : 'audio-out'"
|
||||
:style="{ width: audioContainerWidth + 'px' }"
|
||||
@tap="handlePlayAudio"
|
||||
>
|
||||
<div class="audio-dur">{{ duration }}s</div>
|
||||
<div class="audio-icon-wrapper">
|
||||
<Icon :size="24" :key="audioIconType" :type="audioIconType" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 音频消息组件 */
|
||||
import { ref, onUnmounted, computed, watch, withDefaults } from 'vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMMessageAudioAttachment } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
|
||||
import { onHide, onUnload } from '@dcloudio/uni-app'
|
||||
import { isHarmonyOs } from '@/utils/im/index'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msg: V2NIMMessageForUI
|
||||
mode?: 'audio-in' | 'audio-out'
|
||||
broadcastNewAudioSrc?: string
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
const audioIconType = ref('icon-yuyin3')
|
||||
const animationFlag = ref(false)
|
||||
const isAudioPlaying = ref<boolean>(false)
|
||||
const audioMap = new Map<string, any>()
|
||||
const emits = defineEmits(['getGlobalAudioContext'])
|
||||
|
||||
/** 格式化音频时长 */
|
||||
const formatDuration = (duration: number) => {
|
||||
return Math.round(duration / 1000) || 1
|
||||
}
|
||||
|
||||
/** 音频消息宽度 */
|
||||
const audioContainerWidth = computed(() => {
|
||||
//@ts-ignore
|
||||
const duration = formatDuration(props.msg.attachment?.duration)
|
||||
const maxWidth = 180
|
||||
return 50 + 8 * (duration - 1) > maxWidth ? maxWidth : 50 + 8 * (duration - 1)
|
||||
})
|
||||
|
||||
/** 音频时长 */
|
||||
const duration = computed(() => {
|
||||
return formatDuration(
|
||||
(props.msg.attachment as V2NIMMessageAudioAttachment)?.duration
|
||||
)
|
||||
})
|
||||
|
||||
/**播放音频 */
|
||||
const handlePlayAudio = () => {
|
||||
//@ts-ignore
|
||||
uni.$emit(events.AUDIO_URL_CHANGE, props.msg?.attachment?.url)
|
||||
const audioContext = getAudio()
|
||||
if (!audioContext) {
|
||||
const globalAudioContext = uni.createInnerAudioContext()
|
||||
audioMap.set('audio', globalAudioContext)
|
||||
initAudioSrc()
|
||||
}
|
||||
toggleAudioPlayState()
|
||||
}
|
||||
|
||||
/** 监听当前的音频播放 是不是当前点击url,如果不是,就停止 */
|
||||
watch(
|
||||
() => props.broadcastNewAudioSrc,
|
||||
(newSrc: string) => {
|
||||
//@ts-ignore
|
||||
if (newSrc !== props.msg?.attachment?.url && isAudioPlaying.value) {
|
||||
stopAudio()
|
||||
isAudioPlaying.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/** 播放 */
|
||||
const toggleAudioPlayState = () => {
|
||||
if (!isAudioPlaying.value) {
|
||||
playAudio()
|
||||
} else {
|
||||
stopAudio()
|
||||
}
|
||||
}
|
||||
|
||||
/**停止播放音频 */
|
||||
const stopAudio = () => {
|
||||
const audioContext = getAudio()
|
||||
if (!audioContext) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
audioContext.stop()
|
||||
isAudioPlaying.value = false
|
||||
} catch {
|
||||
console.log('stop audio error')
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化音频实例 */
|
||||
function initAudioSrc() {
|
||||
const audioContext = getAudio()
|
||||
if (!audioContext) {
|
||||
return
|
||||
}
|
||||
//@ts-ignore
|
||||
audioContext.src = props.msg?.attachment?.url
|
||||
isAudioPlaying.value = false
|
||||
audioContext.onPlay(onAudioPlay)
|
||||
audioContext.onStop(onAudioStop)
|
||||
audioContext.onEnded(onAudioEnded)
|
||||
audioContext.onError(onAudioError)
|
||||
}
|
||||
|
||||
/**播放音频 */
|
||||
function playAudio() {
|
||||
const audioContext = getAudio()
|
||||
console.log('audio played', audioContext)
|
||||
|
||||
if (!audioContext) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
audioContext.play()
|
||||
} catch (error) {
|
||||
console.log('audio played error', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 音频开始播放 */
|
||||
function onAudioPlay() {
|
||||
isAudioPlaying.value = true
|
||||
playAudioAnimation()
|
||||
}
|
||||
|
||||
/** 音频停止播放 */
|
||||
function onAudioStop() {
|
||||
animationFlag.value = false
|
||||
isAudioPlaying.value = false
|
||||
if (isHarmonyOs) {
|
||||
const audioContext = getAudio()
|
||||
audioContext?.destroy?.()
|
||||
audioMap.delete('audio')
|
||||
}
|
||||
}
|
||||
|
||||
/** 音频播放结束 */
|
||||
function onAudioEnded() {
|
||||
animationFlag.value = false
|
||||
isAudioPlaying.value = false
|
||||
}
|
||||
|
||||
/** 音频播放失败 */
|
||||
function onAudioError(error: any) {
|
||||
animationFlag.value = false
|
||||
console.warn('audio played error', error)
|
||||
}
|
||||
/**获取音频实例 */
|
||||
const getAudio = () => {
|
||||
return audioMap.get('audio')
|
||||
}
|
||||
|
||||
/** 播放音频动画 */
|
||||
const playAudioAnimation = () => {
|
||||
try {
|
||||
animationFlag.value = true
|
||||
let audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3']
|
||||
const handler = () => {
|
||||
const icon = audioIcons.shift()
|
||||
if (icon) {
|
||||
audioIconType.value = icon
|
||||
if (!audioIcons.length && animationFlag.value) {
|
||||
audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3']
|
||||
}
|
||||
if (audioIcons.length) {
|
||||
setTimeout(handler, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler()
|
||||
} catch (error) {
|
||||
console.log('playAudioAnimation error', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 离开当前页面时,停止播放音频 */
|
||||
const stopAudioOnHide = () => {
|
||||
const audioContext = getAudio()
|
||||
if (isAudioPlaying.value) {
|
||||
stopAudio()
|
||||
}
|
||||
audioContext?.destroy?.()
|
||||
animationFlag.value = false
|
||||
audioMap.delete('audio')
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAudioOnHide()
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
stopAudioOnHide()
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
stopAudioOnHide()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.audio-dur {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.audio-in,
|
||||
.audio-out {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.audio-in {
|
||||
flex-direction: row-reverse;
|
||||
.audio-icon-wrapper {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.audio-icon-wrapper {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<Avatar
|
||||
:account="account"
|
||||
:teamId="teamId"
|
||||
:size="size"
|
||||
:gotoUserCard="gotoUserCard"
|
||||
:onLongpress="handleAitTeamMember"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineProps } from 'vue'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
account: string
|
||||
teamId?: string
|
||||
avatar?: string
|
||||
size?: string
|
||||
gotoUserCard?: boolean
|
||||
fontSize?: string
|
||||
}>()
|
||||
|
||||
const store = uni.$UIKitStore
|
||||
|
||||
const handleAitTeamMember = () => {
|
||||
const isSelf = props.account === store.userStore.myUserInfo.account
|
||||
if (props.teamId && props.account && !isSelf) {
|
||||
uni.$emit(events.AIT_TEAM_MEMBER, {
|
||||
account: props.account,
|
||||
appellation: store.uiStore.getAppellation({
|
||||
account: props.account,
|
||||
teamId: props.teamId,
|
||||
ignoreAlias: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,883 @@
|
||||
<template>
|
||||
<Tooltip
|
||||
v-if="!props.msg.isSelf"
|
||||
:placement="placement"
|
||||
ref="tooltipRef"
|
||||
color="white"
|
||||
>
|
||||
<template #content>
|
||||
<div class="msg-action-groups" v-if="!isUnknownMsg">
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
@tap="handleCopy"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-fuzhi1"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
class="msg-action-btn"
|
||||
@tap="handleReplyMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-huifu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('replyText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
class="msg-action-btn"
|
||||
@tap="handleForwardMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-zhuanfa"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('forwardText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
@tap="handlePinMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-pin"
|
||||
></Icon>
|
||||
<!-- pinState 为 0 或者 undefined,显示“标记”,其他显示“取消标记” -->
|
||||
<text class="msg-action-btn-text">{{
|
||||
props.msg.pinState ? t('unpinText') : t('pinText')
|
||||
}}</text>
|
||||
</div>
|
||||
<div class="msg-action-btn" @tap="handleDeleteMsg">
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-shanchu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
@tap="handleCollectionMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-collection"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('collectionText') }}</text>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 未知消息体 -->
|
||||
<div class="msg-action-groups-unknown" v-else>
|
||||
<div class="msg-action-btn" @tap="handleDeleteMsg">
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-shanchu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="bgVisible" class="msg-bg msg-bg-in">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot v-else></slot>
|
||||
</Tooltip>
|
||||
<div
|
||||
v-else-if="
|
||||
props.msg.sendingState ===
|
||||
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING
|
||||
"
|
||||
class="msg-status-wrapper"
|
||||
>
|
||||
<Icon
|
||||
:size="21"
|
||||
color="#337EFF"
|
||||
class="msg-status-icon icon-loading"
|
||||
type="icon-a-Frame8"
|
||||
></Icon>
|
||||
<Tooltip
|
||||
:placement="placement"
|
||||
ref="tooltipRef"
|
||||
color="white"
|
||||
:align="props.msg.isSelf"
|
||||
>
|
||||
<template #content>
|
||||
<div class="msg-action-groups">
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
@tap="handleCopy"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-fuzhi1"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
|
||||
</div>
|
||||
<div class="msg-action-btn" @tap="handleDeleteMsg">
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-shanchu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="bgVisible" class="msg-bg msg-bg-out">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot v-else></slot>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
props.msg.sendingState ===
|
||||
V2NIMConst.V2NIMMessageSendingState
|
||||
.V2NIM_MESSAGE_SENDING_STATE_FAILED ||
|
||||
props.msg.messageStatus.errorCode === 102426 ||
|
||||
props.msg.messageStatus.errorCode === 104404
|
||||
"
|
||||
class="msg-failed-wrapper"
|
||||
>
|
||||
<div class="msg-failed">
|
||||
<div class="msg-status-wrapper" @tap="handleResendMsg">
|
||||
<div class="icon-fail">!</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
:placement="placement"
|
||||
ref="tooltipRef"
|
||||
color="white"
|
||||
:align="props.msg.isSelf"
|
||||
>
|
||||
<template #content>
|
||||
<div
|
||||
class="msg-action-groups"
|
||||
:style="{
|
||||
width:
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
? '112px'
|
||||
: '56px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
@tap="handleCopy"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-fuzhi1"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
|
||||
</div>
|
||||
<div class="msg-action-btn" @tap="handleDeleteMsg">
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-shanchu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="bgVisible" class="msg-bg msg-bg-out">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot v-else></slot>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
class="in-blacklist"
|
||||
v-if="props.msg.messageStatus.errorCode === 102426"
|
||||
>
|
||||
{{ t('sendFailWithInBlackText') }}
|
||||
</div>
|
||||
<div
|
||||
class="friend-delete"
|
||||
v-else-if="props.msg.messageStatus.errorCode === 104404"
|
||||
>
|
||||
{{ t('sendFailWithDeleteText') }}
|
||||
<span @tap="addFriend" class="friend-verification">{{
|
||||
t('friendVerificationText')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
v-else-if="tooltipVisible"
|
||||
:placement="placement"
|
||||
ref="tooltipRef"
|
||||
color="white"
|
||||
:align="props.msg.isSelf"
|
||||
>
|
||||
<template #content>
|
||||
<div class="msg-action-groups" v-if="!isUnknownMsg">
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
@tap="handleCopy"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-fuzhi1"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
class="msg-action-btn"
|
||||
@tap="handleReplyMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-huifu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('replyText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
class="msg-action-btn"
|
||||
@tap="handleForwardMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-zhuanfa"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('forwardText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
@tap="handlePinMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-pin"
|
||||
></Icon>
|
||||
<!-- pinState 为 0 或者 undefined,显示“标记”,其他显示“取消标记” -->
|
||||
<text class="msg-action-btn-text">{{
|
||||
props.msg.pinState ? t('unpinText') : t('pinText')
|
||||
}}</text>
|
||||
</div>
|
||||
<div class="msg-action-btn" @tap="handleDeleteMsg">
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-shanchu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
class="msg-action-btn"
|
||||
@tap="handleRecallMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-chehui"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('recallText') }}</text>
|
||||
</div>
|
||||
<div
|
||||
class="msg-action-btn"
|
||||
v-if="
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
@tap="handleCollectionMsg"
|
||||
>
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-collection"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('collectionText') }}</text>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 未知消息体 -->
|
||||
<div class="msg-action-groups-unknown" v-else>
|
||||
<div class="msg-action-btn" @tap="handleDeleteMsg">
|
||||
<Icon
|
||||
:size="18"
|
||||
color="#656A72"
|
||||
class="msg-action-btn-icon"
|
||||
type="icon-shanchu"
|
||||
></Icon>
|
||||
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="bgVisible" class="msg-bg msg-bg-out">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot v-else></slot>
|
||||
</Tooltip>
|
||||
<div v-else-if="bgVisible" class="msg-bg msg-bg-out">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 消息操作组件 */
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
// @ts-ignore
|
||||
import Tooltip from '@/components/Tooltip.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import { autorun } from 'mobx'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
//@ts-ignore
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import { msgRecallTime } from '@/utils/im/constants'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
|
||||
const tooltipRef = ref(null)
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msg: V2NIMMessageForUI
|
||||
tooltipVisible?: boolean
|
||||
bgVisible?: boolean
|
||||
placement?: string
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
/**会话类型 */
|
||||
const conversationType =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
|
||||
props.msg.conversationId
|
||||
) as unknown as V2NIMConst.V2NIMConversationType
|
||||
|
||||
onMounted(() => {
|
||||
/** 当前版本仅支持文本、图片、文件、语音、视频 话单消息,其他消息类型统一为未知消息 */
|
||||
isUnknownMsg.value = !(
|
||||
props.msg.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT ||
|
||||
props.msg.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE ||
|
||||
props.msg.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE ||
|
||||
props.msg.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO ||
|
||||
props.msg.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO ||
|
||||
props.msg.messageType == V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
)
|
||||
})
|
||||
|
||||
/** 是否是好友 */
|
||||
const isFriend = ref(true)
|
||||
|
||||
/** 未知消息 */
|
||||
const isUnknownMsg = ref(false)
|
||||
|
||||
const closeTooltip = () => {
|
||||
// @ts-ignore
|
||||
tooltipRef.value.close()
|
||||
}
|
||||
|
||||
/** 复制消息 */
|
||||
const handleCopy = () => {
|
||||
uni.setClipboardData({
|
||||
data: props.msg.text || '',
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('copySuccessText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: t('copyFailText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
complete() {
|
||||
closeTooltip()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 页面滚动到底部 */
|
||||
const scrollBottom = () => {
|
||||
setTimeout(() => {
|
||||
uni.$emit(events.ON_SCROLL_BOTTOM)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/** 重发消息 */
|
||||
const handleResendMsg = async () => {
|
||||
const _msg = props.msg as V2NIMMessageForUI
|
||||
uni.$UIKitStore.msgStore.removeMsg(_msg.conversationId, [
|
||||
_msg.messageClientId,
|
||||
])
|
||||
|
||||
try {
|
||||
if (_msg.threadReply) {
|
||||
const beReplyMsg =
|
||||
await uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers([
|
||||
//@ts-ignore
|
||||
_msg.threadReply,
|
||||
])
|
||||
if (beReplyMsg.length > 0) {
|
||||
//@ts-ignore
|
||||
uni.$UIKitStore.msgStore.replyMsgActive(beReplyMsg[0])
|
||||
}
|
||||
}
|
||||
|
||||
uni.$UIKitStore.msgStore.sendMessageActive({
|
||||
msg: _msg,
|
||||
conversationId: _msg.conversationId,
|
||||
progress: () => true,
|
||||
sendBefore: () => {
|
||||
scrollBottom()
|
||||
},
|
||||
})
|
||||
|
||||
scrollBottom()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 转发消息 */
|
||||
const handleForwardMsg = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: [t('forwardToTeamText'), t('forwardToFriendText')],
|
||||
success(data) {
|
||||
if (data.tapIndex === 0) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}`,
|
||||
})
|
||||
} else if (data.tapIndex === 1) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
complete() {
|
||||
closeTooltip()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** pin消息 */
|
||||
const handlePinMsg = () => {
|
||||
const _msg = props.msg
|
||||
if (_msg.pinState) {
|
||||
// 取消标记
|
||||
uni.$UIKitStore.msgStore.unpinMessageActive(_msg).catch((err: any) => {
|
||||
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
|
||||
uni.showToast({
|
||||
title: t(`${err.code}`),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('unpinFailedText'),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
/** 显示标记 */
|
||||
uni.$UIKitStore.msgStore.pinMessageActive(_msg).catch((err: any) => {
|
||||
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
|
||||
uni.showToast({
|
||||
title: t(`${err.code}`),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('pinFailedText'),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
closeTooltip()
|
||||
}
|
||||
|
||||
/**是否是云端会话 */
|
||||
const enableV2CloudConversation =
|
||||
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
|
||||
/** 收藏消息 */
|
||||
const handleCollectionMsg = () => {
|
||||
const _msg = props.msg
|
||||
|
||||
const conversation = enableV2CloudConversation
|
||||
? uni.$UIKitStore.conversationStore?.conversations.get(_msg.conversationId)
|
||||
: uni.$UIKitStore.localConversationStore?.conversations.get(
|
||||
_msg.conversationId
|
||||
)
|
||||
const collectionDataObj = {
|
||||
//@ts-expect-error
|
||||
message: uni.$UIKitNIM.V2NIMMessageConverter.messageSerialization(_msg), // 序列化
|
||||
avatar: uni.$UIKitStore.userStore.users.get(_msg.senderId)?.avatar,
|
||||
conversationName: conversation?.name,
|
||||
senderName: uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: _msg.senderId,
|
||||
teamId:
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? _msg.receiverId
|
||||
: '',
|
||||
}),
|
||||
}
|
||||
const addCollectionParams = {
|
||||
collectionType: 1000 + _msg.messageType, // 和移动端对齐
|
||||
collectionData: JSON.stringify(collectionDataObj),
|
||||
uniqueId: _msg.messageServerId,
|
||||
}
|
||||
|
||||
uni.$UIKitStore.msgStore
|
||||
.addCollectionActive(addCollectionParams)
|
||||
.then(() => {
|
||||
uni.showToast({
|
||||
title: t('addCollectionSuccessText'),
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
|
||||
uni.showToast({
|
||||
title: t(`${err.code}`),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('addCollectionFailedText'),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
})
|
||||
closeTooltip()
|
||||
}
|
||||
|
||||
/** 回复消息 */
|
||||
const handleReplyMsg = async () => {
|
||||
const _msg = props.msg
|
||||
|
||||
uni.$UIKitStore.msgStore.replyMsgActive(_msg)
|
||||
closeTooltip()
|
||||
uni.$emit(events.REPLY_MSG, props.msg)
|
||||
|
||||
// 在群里回复其他人的消息,也是@被回复人
|
||||
if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
|
||||
!props.msg.isSelf
|
||||
) {
|
||||
uni.$emit(events.AIT_TEAM_MEMBER, {
|
||||
accountId: props.msg.senderId,
|
||||
appellation: uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.senderId,
|
||||
teamId: props.msg.receiverId,
|
||||
ignoreAlias: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 撤回消息 */
|
||||
const handleRecallMsg = () => {
|
||||
const diff = Date.now() - props.msg.createTime
|
||||
if (diff > msgRecallTime) {
|
||||
uni.showToast({
|
||||
title: t('msgRecallTimeErrorText'),
|
||||
icon: 'none',
|
||||
})
|
||||
closeTooltip()
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: t('recallText'),
|
||||
content: t('recall3'),
|
||||
showCancel: true,
|
||||
confirmText: t('recallText'),
|
||||
confirmColor: '#1861df',
|
||||
success(data) {
|
||||
if (data.confirm) {
|
||||
const _msg = props.msg
|
||||
|
||||
uni.$UIKitStore.msgStore.reCallMsgActive(_msg).catch(() => {
|
||||
uni.showToast({
|
||||
title: t('recallMsgFailText'),
|
||||
icon: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
complete() {
|
||||
closeTooltip()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除消息 */
|
||||
const handleDeleteMsg = () => {
|
||||
const _msg = props.msg
|
||||
uni.showModal({
|
||||
title: t('deleteText'),
|
||||
content: t('delete'),
|
||||
showCancel: true,
|
||||
confirmText: t('deleteText'),
|
||||
confirmColor: '#1861df',
|
||||
success(data) {
|
||||
if (data.confirm) {
|
||||
uni.$UIKitStore.msgStore
|
||||
.deleteMsgActive([_msg])
|
||||
.then(() => {
|
||||
uni.showToast({
|
||||
title: t('deleteMsgSuccessText'),
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
.catch((error: any) => {
|
||||
uni.showToast({
|
||||
title: t('deleteMsgFailText'),
|
||||
icon: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
complete() {
|
||||
closeTooltip()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 添加好友 */
|
||||
const addFriend = () => {
|
||||
customNavigateTo({
|
||||
url: `/pages/User/friend/index?account=${props.msg.receiverId}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 监听好友列表 */
|
||||
const friendsWatch = autorun(() => {
|
||||
const _isFriend = uni.$UIKitStore.uiStore.friends
|
||||
.filter(
|
||||
(item) =>
|
||||
!uni.$UIKitStore.relationStore.blacklist.includes(item.accountId)
|
||||
)
|
||||
.map((item) => item.accountId)
|
||||
.some((item: any) => item.account === props.msg.receiverId)
|
||||
isFriend.value = _isFriend
|
||||
})
|
||||
|
||||
/** 卸载监听 */
|
||||
onUnmounted(() => {
|
||||
friendsWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/common.scss';
|
||||
|
||||
.msg-bg {
|
||||
max-width: 360rpx;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
|
||||
&-in {
|
||||
border-radius: 0 8px 8px 8px;
|
||||
background-color: #e8eaed;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&-out {
|
||||
border-radius: 8px 0 8px 8px;
|
||||
background-color: #d6e5f6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-action-groups {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
max-width: 224px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.msg-action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
width: 56px;
|
||||
&-icon {
|
||||
color: #656a72;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-failed-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
|
||||
.in-blacklist {
|
||||
color: #b3b7bc;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
right: 20%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.friend-delete {
|
||||
color: #b3b7bc;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
|
||||
.friend-verification {
|
||||
color: #337eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msg-status-wrapper {
|
||||
// max-width: 450rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
box-sizing: border-box;
|
||||
.msg-bg-out {
|
||||
margin-right: 0;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-status-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 21px;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: -30px;
|
||||
|
||||
&.icon-loading {
|
||||
color: #337eff;
|
||||
animation: loadingCircle 1s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-fail {
|
||||
background: #fc596a;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
margin-right: 5px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.msg-failed {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<uni-link
|
||||
v-if="!isWxApp"
|
||||
class="msg-file-wrapper"
|
||||
:href="downloadUrl"
|
||||
:download="name"
|
||||
:showUnderLine="false"
|
||||
>
|
||||
<div
|
||||
:class="!msg.isSelf ? 'msg-file msg-file-in' : 'msg-file msg-file-out'"
|
||||
@click="() => openInBrowser(downloadUrl)"
|
||||
>
|
||||
<Icon :type="iconType" :size="32"></Icon>
|
||||
<div class="msg-file-content">
|
||||
<div class="msg-file-title">
|
||||
<div class="msg-file-title-prefix">{{ prefixName }}</div>
|
||||
<div class="msg-file-title-suffix">{{ suffixName }}</div>
|
||||
</div>
|
||||
<div class="msg-file-size">{{ parseFileSize(size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</uni-link>
|
||||
<div v-else @click="mpDownload">
|
||||
<div
|
||||
:class="!msg.isSelf ? 'msg-file msg-file-in' : 'msg-file msg-file-out'"
|
||||
>
|
||||
<Icon :type="iconType" :size="32"></Icon>
|
||||
<div class="msg-file-content">
|
||||
<div class="msg-file-title">
|
||||
<div class="msg-file-title-prefix">{{ prefixName }}</div>
|
||||
<div class="msg-file-title-suffix">{{ suffixName }}</div>
|
||||
<!-- <text class="msg-file-name" v-text="name"></text> -->
|
||||
</div>
|
||||
<div class="msg-file-size">{{ parseFileSize(size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 文件消息组件 */
|
||||
|
||||
import { getFileType, parseFileSize } from '@xkit-yx/utils'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
// @ts-ignore
|
||||
import UniLink from '@/components/uni-components/uni-link/components/uni-link/uni-link.vue'
|
||||
import { isWxApp } from '@/utils/im/index'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMMessageFileAttachment } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
|
||||
|
||||
const props = withDefaults(defineProps<{ msg: V2NIMMessageForUI }>(), {})
|
||||
|
||||
/** 文件图标映射 */
|
||||
const fileIconMap = {
|
||||
pdf: 'icon-PPT',
|
||||
word: 'icon-Word',
|
||||
excel: 'icon-Excel',
|
||||
ppt: 'icon-PPT',
|
||||
zip: 'icon-RAR1',
|
||||
txt: 'icon-qita',
|
||||
img: 'icon-tupian2',
|
||||
audio: 'icon-yinle',
|
||||
video: 'icon-shipin',
|
||||
}
|
||||
|
||||
const {
|
||||
name = '',
|
||||
url = '',
|
||||
ext = '',
|
||||
size = 0,
|
||||
} = (props.msg.attachment as V2NIMMessageFileAttachment) || {}
|
||||
|
||||
//@ts-ignore
|
||||
const iconType = fileIconMap[getFileType(ext)] || 'icon-weizhiwenjian'
|
||||
|
||||
const index = name.lastIndexOf('.') > -1 ? name.lastIndexOf('.') : name.length
|
||||
|
||||
/** 文件名前缀 */
|
||||
const prefixName = name.slice(0, Math.max(index - 5, 0))
|
||||
|
||||
/** 文件名后缀 */
|
||||
const suffixName = name.slice(Math.max(index - 5, 0))
|
||||
|
||||
/** 下载地址 */
|
||||
const downloadUrl =
|
||||
url + ((url as string).includes('?') ? '&' : '?') + `download=${name}`
|
||||
|
||||
/** 小程序不支持直接下载文件,复制链接到剪切板,浏览器打开 */
|
||||
const mpDownload = () => {
|
||||
uni.setClipboardData({
|
||||
data: downloadUrl,
|
||||
})
|
||||
uni.showModal({
|
||||
content: t('wxAppFileCopyText'),
|
||||
showCancel: false,
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开浏览器 */
|
||||
const openInBrowser = (url: string) => {
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('openUrlText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-file {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee0e2;
|
||||
|
||||
&-in {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&-out {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
margin-left: 15px;
|
||||
max-width: 300rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
|
||||
&-prefix {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&-suffix {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-size {
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<Modal
|
||||
:title="t('sendToText')"
|
||||
:visible="forwardModalVisible"
|
||||
:confirmText="t('sendText')"
|
||||
:cancelText="t('cancelText')"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
props.forwardConversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
"
|
||||
class="avatar-wrapper"
|
||||
>
|
||||
<Avatar
|
||||
:account="
|
||||
(props.forwardToTeamInfo && props.forwardToTeamInfo.teamId) || ''
|
||||
"
|
||||
:avatar="props.forwardToTeamInfo && props.forwardToTeamInfo.avatar"
|
||||
size="36"
|
||||
/>
|
||||
<div class="name">
|
||||
{{ (props.forwardToTeamInfo && props.forwardToTeamInfo.name) || '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="avatar-wrapper">
|
||||
<Avatar :account="forwardTo" size="36" />
|
||||
<div class="name">
|
||||
<span>{{ forwardToNick }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ '[' + t('forwardText') + ']' }}
|
||||
{{ forwardFromNick }}
|
||||
{{ t('sessionRecordText') }}
|
||||
</div>
|
||||
<input
|
||||
class="forward-input"
|
||||
@input="handleForwardInputChange"
|
||||
:placeholder="t('forwardComment')"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 消息转发弹窗组件 */
|
||||
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { ref, computed } from 'vue'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
|
||||
interface ForwardToTeamInfo {
|
||||
teamId: string
|
||||
name: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
forwardModalVisible: boolean
|
||||
forwardTo: string
|
||||
forwardMsg: V2NIMMessageForUI
|
||||
forwardConversationType: V2NIMConst.V2NIMConversationType
|
||||
forwardToTeamInfo?: ForwardToTeamInfo
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel'])
|
||||
|
||||
/** 留言 */
|
||||
const forwardComment = ref('')
|
||||
|
||||
/** 转发弹窗 Input */
|
||||
const handleForwardInputChange = (event: any) => {
|
||||
forwardComment.value = event.detail.value
|
||||
}
|
||||
/** 取消转发 */
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
/** 确认转发 */
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', forwardComment.value)
|
||||
}
|
||||
|
||||
/** 转发消息的接收方昵称 */
|
||||
const forwardToNick = computed(() => {
|
||||
return uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.forwardTo,
|
||||
})
|
||||
})
|
||||
|
||||
/** 转发消息的发送方昵称 */
|
||||
const forwardFromNick = computed(() => {
|
||||
return uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.forwardMsg?.senderId,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.description {
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
color: #000000;
|
||||
background-color: #f2f4f5;
|
||||
margin: 16px;
|
||||
padding: 0 16px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.forward-input {
|
||||
height: 32px;
|
||||
border: 1px solid #e1e6e8;
|
||||
border-radius: 4px;
|
||||
margin: 10px 16px 0 16px;
|
||||
padding: 5px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin: 13px 16px;
|
||||
.name {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="g2-message-wrapper" @click="handleCall">
|
||||
<Icon :type="iconType" :size="28"></Icon>
|
||||
<div class="g2-message-status">{{ status }}</div>
|
||||
<div v-if="duration" class="g2-message-duration">{{ duration }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 音视频消息组件 */
|
||||
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { convertSecondsToTime, startCall, isApp } from '@/utils/im/index'
|
||||
import { g2StatusMap } from '@/utils/im/constants'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
|
||||
const props = withDefaults(defineProps<{ msg: V2NIMMessageForUI }>(), {})
|
||||
|
||||
/** 通话时长 */
|
||||
const duration = convertSecondsToTime(
|
||||
//@ts-ignore
|
||||
props.msg.attachment?.durations[0]?.duration
|
||||
)
|
||||
/** 通话状态 */
|
||||
//@ts-expect-error
|
||||
const status = g2StatusMap[props.msg.attachment?.status]
|
||||
const iconType =
|
||||
//@ts-expect-error
|
||||
props.msg.attachment?.type == 1 ? 'icon-yuyin8' : 'icon-shipin8'
|
||||
|
||||
/** 发起呼叫 */
|
||||
const handleCall = () => {
|
||||
if (isApp) {
|
||||
//@ts-ignore
|
||||
const callType = props.msg.attachment?.type
|
||||
|
||||
const myAccount = uni.$UIKitStore.userStore.myUserInfo.accountId
|
||||
const isSelfMsg = props.msg.senderId === myAccount
|
||||
|
||||
if (isSelfMsg) {
|
||||
const remoteShowName = uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.receiverId,
|
||||
})
|
||||
|
||||
startCall({
|
||||
remoteUserAccid: props.msg.receiverId,
|
||||
currentUserAccid: myAccount,
|
||||
type: callType,
|
||||
remoteShowName: remoteShowName,
|
||||
})
|
||||
} else {
|
||||
const remoteShowName = uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.senderId,
|
||||
})
|
||||
startCall({
|
||||
remoteUserAccid: props.msg.senderId,
|
||||
currentUserAccid: myAccount,
|
||||
type: callType,
|
||||
remoteShowName: remoteShowName,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('callFailedText'),
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.g2-message-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.g2-message-status {
|
||||
margin: 0 7px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,664 @@
|
||||
<template>
|
||||
<div
|
||||
:class="`msg-item-wrapper ${
|
||||
props.msg.pinState &&
|
||||
!(
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
props.msg.timeValue !== undefined
|
||||
) &&
|
||||
!props.msg.recallType
|
||||
? 'msg-pin'
|
||||
: ''
|
||||
}`"
|
||||
:id="MSG_ID_FLAG + props.msg.messageClientId"
|
||||
:key="props.msg.createTime"
|
||||
>
|
||||
<!-- 消息时间 -->
|
||||
<div
|
||||
class="msg-time"
|
||||
v-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
props.msg.timeValue !== undefined
|
||||
"
|
||||
>
|
||||
{{ props.msg.timeValue }}
|
||||
</div>
|
||||
<!-- 撤回消息 可重新编辑 -->
|
||||
<div
|
||||
class="msg-common"
|
||||
:style="{
|
||||
flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse',
|
||||
}"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
props.msg.recallType === 'reCallMsg' &&
|
||||
props.msg.canEdit
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<MessageBubble :msg="props.msg" :bg-visible="true">
|
||||
{{ t('recall2') }}
|
||||
<text
|
||||
class="msg-recall-btn"
|
||||
@tap="
|
||||
() => {
|
||||
handleReeditMsg(props.msg)
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ t('reeditText') }}
|
||||
</text>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<!-- 撤回消息 不可重新编辑 主动撤回 -->
|
||||
<div
|
||||
class="msg-common"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
props.msg.recallType === 'reCallMsg' &&
|
||||
!props.msg.canEdit
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<MessageBubble :msg="props.msg" :bg-visible="true">
|
||||
<div class="recall-text">{{ t('you') + t('recall') }}</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<!-- 撤回消息 对方撤回-->
|
||||
<div
|
||||
class="msg-common"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
props.msg.recallType === 'beReCallMsg'
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<div :class="props.msg.isSelf ? 'self-msg-recall' : 'msg-recall'">
|
||||
<text class="msg-recall2">
|
||||
{{ !props.msg.isSelf ? t('recall2') : `${t('you') + t('recall')}` }}
|
||||
</text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文本消息-->
|
||||
<div
|
||||
class="msg-common"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="true"
|
||||
>
|
||||
<ReplyMessage v-if="!!replyMsg" :replyMsg="replyMsg"></ReplyMessage>
|
||||
<MessageText :msg="props.msg"></MessageText>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
|
||||
</div>
|
||||
<!-- 图片消息-->
|
||||
<div
|
||||
class="msg-common"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
|
||||
"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="true"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<div
|
||||
@tap="
|
||||
() => {
|
||||
//@ts-ignore
|
||||
handleImageTouch(props.msg.attachment?.url)
|
||||
}
|
||||
"
|
||||
>
|
||||
<image
|
||||
class="msg-image"
|
||||
:lazy-load="true"
|
||||
mode="aspectFill"
|
||||
:src="imageUrl"
|
||||
></image>
|
||||
</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
|
||||
</div>
|
||||
<!-- 视频消息-->
|
||||
<div
|
||||
class="msg-common"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
|
||||
"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="true"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<div
|
||||
class="video-msg-wrapper"
|
||||
@tap="() => handleVideoTouch(props.msg)"
|
||||
>
|
||||
<div class="video-play-button">
|
||||
<div class="video-play-icon"></div>
|
||||
</div>
|
||||
<image
|
||||
class="msg-image"
|
||||
:lazy-load="true"
|
||||
mode="aspectFill"
|
||||
:src="videoFirstFrameDataUrl"
|
||||
></image>
|
||||
</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
|
||||
</div>
|
||||
<!-- 音视频消息-->
|
||||
<div
|
||||
class="msg-common"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
|
||||
"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="true"
|
||||
>
|
||||
<MessageG2 :msg="props.msg" />
|
||||
</MessageBubble>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件消息-->
|
||||
<div
|
||||
class="msg-common"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
|
||||
"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="false"
|
||||
>
|
||||
<MessageFile :msg="props.msg" />
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
|
||||
</div>
|
||||
<!-- 语音消息-->
|
||||
<div
|
||||
class="msg-common"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
|
||||
"
|
||||
:style="{
|
||||
flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse',
|
||||
}"
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="true"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<MessageAudio
|
||||
:msg="props.msg"
|
||||
:broadcastNewAudioSrc="broadcastNewAudioSrc"
|
||||
/>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
<!-- <MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead> -->
|
||||
</div>
|
||||
<!-- 通知消息-->
|
||||
<MessageNotification
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION
|
||||
"
|
||||
:msg="props.msg"
|
||||
/>
|
||||
<div
|
||||
class="msg-common"
|
||||
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
|
||||
v-else
|
||||
>
|
||||
<Avatar
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
:goto-user-card="true"
|
||||
/>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name" v-if="!props.msg.isSelf">
|
||||
{{ appellation }}
|
||||
</div>
|
||||
<MessageBubble
|
||||
:msg="props.msg"
|
||||
:tooltip-visible="true"
|
||||
:bg-visible="true"
|
||||
>
|
||||
[{{ t('unknowMsgText') }}]
|
||||
</MessageBubble>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息标记 不展示 pinState 为 0 、时间消息以及撤回消息的标记样式 -->
|
||||
<div
|
||||
class="msg-pin-tip"
|
||||
v-if="
|
||||
props.msg.pinState &&
|
||||
!(
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
props.msg.timeValue !== undefined
|
||||
) &&
|
||||
!props.msg.recallType
|
||||
"
|
||||
:style="{ justifyContent: !props.msg.isSelf ? 'flex-start' : 'flex-end' }"
|
||||
>
|
||||
<Icon :size="11" type="icon-green-pin"></Icon> <span
|
||||
v-if="props.msg.operatorId === accountId"
|
||||
>{{ `${t('you')}` }}</span
|
||||
>
|
||||
<Appellation
|
||||
v-else
|
||||
:account="props.msg.operatorId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
color="#3EAF96"
|
||||
fontSize="11"
|
||||
></Appellation
|
||||
> {{ `${t('pinThisText')}` }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 消息组件 */
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import MessageBubble from './message-bubble.vue'
|
||||
import ReplyMessage from './message-reply.vue'
|
||||
import MessageFile from './message-file.vue'
|
||||
import MessageText from './message-text.vue'
|
||||
import MessageAudio from './message-audio.vue'
|
||||
import MessageNotification from './message-notification.vue'
|
||||
import MessageG2 from './message-g2.vue'
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import MessageIsRead from './message-read.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import Appellation from '@/components/Appellation.vue'
|
||||
import { events, MSG_ID_FLAG } from '@/utils/im/constants'
|
||||
import { autorun } from 'mobx'
|
||||
import { stopAllAudio } from '@/utils/im/index'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msg: V2NIMMessageForUI & { timeValue?: number }
|
||||
index: number
|
||||
replyMsgsMap?: {
|
||||
[key: string]: V2NIMMessageForUI
|
||||
}
|
||||
broadcastNewAudioSrc: string
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
/** 回复消息 */
|
||||
const replyMsg = computed(() => {
|
||||
return props.replyMsgsMap && props.replyMsgsMap[props.msg.messageClientId]
|
||||
})
|
||||
|
||||
/** 昵称 */
|
||||
const appellation = ref('')
|
||||
/** 当前用户账号 */
|
||||
const accountId = uni.$UIKitStore?.userStore?.myUserInfo.accountId
|
||||
|
||||
/** 会话类型 */
|
||||
const conversationType =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
|
||||
props.msg.conversationId
|
||||
) as unknown as V2NIMConst.V2NIMConversationType
|
||||
/** 会话对象 */
|
||||
const to = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
|
||||
props.msg.conversationId
|
||||
)
|
||||
|
||||
/** 获取视频首帧 */
|
||||
const videoFirstFrameDataUrl = computed(() => {
|
||||
//@ts-ignore
|
||||
const url = props.msg.attachment?.url
|
||||
return url ? `${url}${url.includes('?') ? '&' : '?'}vframe&offset=1` : ''
|
||||
})
|
||||
|
||||
/** 图片地址 */
|
||||
const imageUrl = computed(() => {
|
||||
/** 被拉黑 */
|
||||
if (props.msg.messageStatus.errorCode == 102426) {
|
||||
return 'https://yx-web-nosdn.netease.im/common/c1f278b963b18667ecba4ee9a6e68047/img-fail.png'
|
||||
}
|
||||
|
||||
/** 非好友关系 */
|
||||
if (props.msg.messageStatus.errorCode == 104404) {
|
||||
return 'https://yx-web-nosdn.netease.im/common/c1f278b963b18667ecba4ee9a6e68047/img-fail.png'
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
return props.msg?.attachment?.url || props.msg.attachment?.file
|
||||
})
|
||||
|
||||
/** 点击图片预览 */
|
||||
const handleImageTouch = (url: string) => {
|
||||
if (url) {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击视频播放 */
|
||||
const handleVideoTouch = (msg: V2NIMMessageForUI) => {
|
||||
stopAllAudio()
|
||||
//@ts-ignore
|
||||
const url = msg.attachment?.url
|
||||
if (url) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/video-play?videoUrl=${encodeURIComponent(url)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 重新编辑消息 */
|
||||
const handleReeditMsg = (msg: V2NIMMessageForUI) => {
|
||||
uni.$emit(events.ON_REEDIT_MSG, msg)
|
||||
}
|
||||
|
||||
/** 监听昵称变化 */
|
||||
const appellationWatch = autorun(() => {
|
||||
/** 昵称展示顺序 群昵称 > 备注 > 个人昵称 > 帐号 */
|
||||
appellation.value = uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.senderId,
|
||||
teamId:
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: '',
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
appellationWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-item-wrapper {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.msg-common {
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 16px;
|
||||
message-is-read {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
.msg-pin {
|
||||
opacity: 1;
|
||||
background: #fffbea;
|
||||
}
|
||||
.msg-pin-tip {
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
color: #3eaf96;
|
||||
margin: 8px 50px 0 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.msg-name {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: left;
|
||||
margin-bottom: 4px;
|
||||
max-width: 300rpx;
|
||||
padding-left: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.msg-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
color: #b3b7bc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.msg-recall-btn {
|
||||
margin-left: 5px;
|
||||
color: #1861df;
|
||||
}
|
||||
|
||||
.msg-recall2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.self-msg-recall {
|
||||
max-width: 360rpx;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px 0px 8px 8px;
|
||||
margin-right: 8px;
|
||||
background-color: #d6e5f6;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.msg-recall {
|
||||
max-width: 360rpx;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
border-radius: 0px 8px 8px 8px;
|
||||
margin-left: 8px;
|
||||
background-color: #e8eaed;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.recall-text {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.video-play-button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.video-play-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-left: 18px solid #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-40%, -50%);
|
||||
}
|
||||
|
||||
.video-msg-wrapper {
|
||||
box-sizing: border-box;
|
||||
max-width: 360rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="msg-list-wrapper" @touchstart="handleTapMessageList">
|
||||
<scroll-view
|
||||
id="message-scroll-list"
|
||||
scroll-y="true"
|
||||
:scroll-top="scrollTop"
|
||||
class="message-scroll-list"
|
||||
>
|
||||
<!-- 查看更多 -->
|
||||
<div v-show="!noMore" @click="onLoadMore" class="view-more-text">
|
||||
{{ t('viewMoreText') }}
|
||||
</div>
|
||||
<view class="msg-tip" v-show="noMore">{{ t('noMoreText') }}</view>
|
||||
<div v-for="(item, index) in finalMsgs" :key="item.renderKey">
|
||||
<MessageItem
|
||||
:msg="item"
|
||||
:index="index"
|
||||
:key="item.renderKey"
|
||||
:reply-msgs-map="replyMsgsMap"
|
||||
:broadcastNewAudioSrc="broadcastNewAudioSrc"
|
||||
>
|
||||
</MessageItem>
|
||||
</div>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 消息列表组件 */
|
||||
|
||||
import { ref, computed, onBeforeMount, onUnmounted, withDefaults } from 'vue'
|
||||
import MessageItem from './message-item.vue'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import { caculateTimeago } from '@/utils/im/date'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import { V2NIMTeam } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
|
||||
import { autorun } from 'mobx'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msgs: V2NIMMessageForUI[]
|
||||
conversationType: V2NIMConst.V2NIMConversationType
|
||||
to: string
|
||||
loadingMore?: boolean
|
||||
noMore?: boolean
|
||||
replyMsgsMap?: {
|
||||
[key: string]: V2NIMMessageForUI
|
||||
}
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
/** 群信息监听 */
|
||||
let teamWatch = () => {}
|
||||
|
||||
onBeforeMount(() => {
|
||||
let team: V2NIMTeam | undefined = undefined
|
||||
/** 群监听 */
|
||||
teamWatch = autorun(() => {
|
||||
team = uni.$UIKitStore.teamStore.teams.get(props.to) as unknown as V2NIMTeam
|
||||
})
|
||||
|
||||
if (
|
||||
props.conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
) {
|
||||
uni.$UIKitStore.teamMemberStore.getTeamMemberActive({
|
||||
teamId: props.to,
|
||||
queryOption: {
|
||||
limit: Math.max((team as unknown as V2NIMTeam)?.memberLimit || 0, 200),
|
||||
roleQueryType: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
/** 全局播放音频url */
|
||||
uni.$on(events.AUDIO_URL_CHANGE, (url) => {
|
||||
broadcastNewAudioSrc.value = url
|
||||
})
|
||||
|
||||
/** 滚动到底部 */
|
||||
uni.$on(events.ON_SCROLL_BOTTOM, () => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
/** 加载更多 */
|
||||
uni.$on(events.ON_LOAD_MORE, () => {
|
||||
const msg = finalMsgs.value.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
|
||||
)
|
||||
)[0]
|
||||
if (msg) {
|
||||
uni.$emit(events.GET_HISTORY_MSG, msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/** 滚动条位置距离 */
|
||||
const scrollTop = ref(99999)
|
||||
|
||||
/** 消息列表 */
|
||||
const finalMsgs = computed(() => {
|
||||
const res: (V2NIMMessageForUI & { renderKey: string })[] = []
|
||||
props.msgs.forEach((item, index) => {
|
||||
// 如果两条消息间隔超过5分钟,插入一条自定义时间消息
|
||||
if (
|
||||
index > 0 &&
|
||||
item.createTime - props.msgs[index - 1].createTime > 5 * 60 * 1000
|
||||
) {
|
||||
res.push({
|
||||
...item,
|
||||
messageClientId: 'time-' + item.createTime,
|
||||
messageType: V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM,
|
||||
sendingState:
|
||||
V2NIMConst.V2NIMMessageSendingState
|
||||
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED,
|
||||
// @ts-ignore
|
||||
timeValue: caculateTimeago(item.createTime),
|
||||
renderKey: `${item.createTime + 1}`,
|
||||
})
|
||||
}
|
||||
res.push({
|
||||
...item,
|
||||
// @ts-ignore
|
||||
renderKey: `${item.createTime}`,
|
||||
})
|
||||
})
|
||||
|
||||
return res
|
||||
})
|
||||
|
||||
/** 全局播放音频url */
|
||||
const broadcastNewAudioSrc = ref<string>('')
|
||||
|
||||
/** 消息滑动到底部
|
||||
* 不建议查询当前的消息列表dom高度进行滚动,在个别机型会不生效,并有卡顿问题,使用极大值,进行滚动,无该问题
|
||||
*/
|
||||
const scrollToBottom = () => {
|
||||
scrollTop.value += 9999999
|
||||
const timer = setTimeout(() => {
|
||||
scrollTop.value += 1
|
||||
clearTimeout(timer)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 加载更多消息 */
|
||||
const onLoadMore = () => {
|
||||
const msg = finalMsgs.value.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
|
||||
['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
|
||||
)
|
||||
)[0]
|
||||
uni.$emit(events.GET_HISTORY_MSG, msg)
|
||||
}
|
||||
|
||||
/** 点击消息列表 */
|
||||
const handleTapMessageList = () => {
|
||||
uni.$emit(events.CLOSE_PANEL)
|
||||
setTimeout(() => {
|
||||
uni.$emit(events.CLOSE_PANEL)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off(events.ON_SCROLL_BOTTOM)
|
||||
|
||||
uni.$off(events.ON_LOAD_MORE)
|
||||
|
||||
uni.$off(events.AUDIO_URL_CHANGE)
|
||||
|
||||
teamWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-list-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px 0;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
.msg-tip {
|
||||
text-align: center;
|
||||
color: #b3b7bc;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.message-scroll-list {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.view-more-text {
|
||||
text-align: center;
|
||||
color: #b3b7bc;
|
||||
font-size: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
page > view > message > view > message-list {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div v-if="notificationContent" class="msg-noti">
|
||||
{{ notificationContent }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 通知消息组件 */
|
||||
|
||||
import { ALLOW_AT } from '@/utils/im/constants'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import { V2NIMTeam } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
|
||||
import {
|
||||
V2NIMMessageForUI,
|
||||
YxServerExt,
|
||||
} from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMMessageNotificationAttachment } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { autorun } from 'mobx'
|
||||
const props = withDefaults(defineProps<{ msg: V2NIMMessageForUI }>(), {})
|
||||
|
||||
/** 群ID */
|
||||
const teamId =
|
||||
props.msg.conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? props.msg.receiverId
|
||||
: ''
|
||||
|
||||
/** 通知消息内容 */
|
||||
const notificationContent = ref('')
|
||||
|
||||
/** 通知消息监听 */
|
||||
const notificationContentWatch = autorun(() => {
|
||||
const getNotificationContent = () => {
|
||||
const attachment = props.msg
|
||||
.attachment as V2NIMMessageNotificationAttachment
|
||||
switch (attachment?.type) {
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO: {
|
||||
const team = (attachment?.updatedTeamInfo || {}) as V2NIMTeam
|
||||
const content: string[] = []
|
||||
|
||||
if (team.avatar !== undefined) {
|
||||
content.push(t('updateTeamAvatar'))
|
||||
}
|
||||
if (team.name !== undefined) {
|
||||
content.push(`${t('updateTeamName')}“${team.name}”`)
|
||||
}
|
||||
if (team.intro !== undefined) {
|
||||
content.push(t('updateTeamIntro'))
|
||||
}
|
||||
if (team.inviteMode !== undefined) {
|
||||
content.push(
|
||||
`${t('updateTeamInviteMode')}“${
|
||||
team.inviteMode ===
|
||||
V2NIMConst.V2NIMTeamInviteMode.V2NIM_TEAM_INVITE_MODE_ALL
|
||||
? t('teamAll')
|
||||
: t('teamOwnerAndManagerText')
|
||||
}”`
|
||||
)
|
||||
}
|
||||
if (team.updateInfoMode !== undefined) {
|
||||
content.push(
|
||||
`${t('updateTeamUpdateTeamMode')}“${
|
||||
team.updateInfoMode ===
|
||||
V2NIMConst.V2NIMTeamUpdateInfoMode.V2NIM_TEAM_UPDATE_INFO_MODE_ALL
|
||||
? t('teamAll')
|
||||
: t('teamOwnerAndManagerText')
|
||||
}”`
|
||||
)
|
||||
}
|
||||
if (team.chatBannedMode !== void 0) {
|
||||
content.push(
|
||||
`${t('updateTeamMute')}${
|
||||
team.chatBannedMode ===
|
||||
V2NIMConst.V2NIMTeamChatBannedMode
|
||||
.V2NIM_TEAM_CHAT_BANNED_MODE_UNBAN
|
||||
? t('closeText')
|
||||
: t('openText')
|
||||
}`
|
||||
)
|
||||
}
|
||||
if (team.serverExtension) {
|
||||
let ext: YxServerExt = {}
|
||||
try {
|
||||
ext = JSON.parse(team.serverExtension)
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
if (ext[ALLOW_AT] !== undefined) {
|
||||
content.push(
|
||||
`${t('updateAllowAt')}“${
|
||||
ext[ALLOW_AT] === 'manager'
|
||||
? t('teamOwnerAndManagerText')
|
||||
: t('teamAll')
|
||||
}”`
|
||||
)
|
||||
}
|
||||
}
|
||||
return content.length
|
||||
? `${uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.senderId,
|
||||
teamId,
|
||||
})} ${content.join('、')}`
|
||||
: ''
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS:
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE_ACCEPT: {
|
||||
return `${uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.senderId,
|
||||
teamId,
|
||||
})} ${t('joinTeamText')}`
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE: {
|
||||
const accounts: string[] = attachment?.targetIds || []
|
||||
accounts.map(async (item) => {
|
||||
await uni.$UIKitStore.userStore.getUserActive(item)
|
||||
})
|
||||
const nicks = accounts
|
||||
.map((item) => {
|
||||
return uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: item,
|
||||
teamId,
|
||||
})
|
||||
})
|
||||
.filter((item) => !!item)
|
||||
.join('、')
|
||||
|
||||
return `${nicks} ${t('joinTeamText')}`
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_KICK: {
|
||||
const accounts: string[] = attachment?.targetIds || []
|
||||
accounts.map(async (item) => {
|
||||
await uni.$UIKitStore.userStore.getUserActive(item)
|
||||
})
|
||||
const nicks = accounts
|
||||
.map((item) => {
|
||||
return uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: item,
|
||||
teamId,
|
||||
})
|
||||
})
|
||||
.filter((item) => !!item)
|
||||
.join('、')
|
||||
|
||||
return `${nicks} ${t('beRemoveTeamText')}`
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_ADD_MANAGER: {
|
||||
const accounts: string[] = attachment?.targetIds || []
|
||||
accounts.map(async (item) => {
|
||||
await uni.$UIKitStore.userStore.getUserActive(item)
|
||||
})
|
||||
const nicks = accounts
|
||||
.map((item) => {
|
||||
return uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: item,
|
||||
teamId,
|
||||
})
|
||||
})
|
||||
.filter((item) => !!item)
|
||||
.join('、')
|
||||
|
||||
return `${nicks} ${t('beAddTeamManagersText')}`
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_REMOVE_MANAGER: {
|
||||
const accounts: string[] = attachment?.targetIds || []
|
||||
accounts.map(async (item) => {
|
||||
await uni.$UIKitStore.userStore.getUserActive(item)
|
||||
})
|
||||
const nicks = accounts
|
||||
.map((item) => {
|
||||
return uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: item,
|
||||
teamId,
|
||||
})
|
||||
})
|
||||
.filter((item) => !!item)
|
||||
.join('、')
|
||||
|
||||
return `${nicks} ${t('beRemoveTeamManagersText')}`
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: {
|
||||
return `${uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.msg.senderId,
|
||||
teamId,
|
||||
})} ${t('leaveTeamText')}`
|
||||
}
|
||||
case V2NIMConst.V2NIMMessageNotificationType
|
||||
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_OWNER_TRANSFER: {
|
||||
return `${uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: (attachment?.targetIds || [])[0],
|
||||
teamId,
|
||||
})} ${t('newGroupOwnerText')}`
|
||||
}
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
notificationContent.value = getNotificationContent()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
notificationContentWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-noti {
|
||||
margin: 8px auto 0;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #b3b7bc;
|
||||
max-width: 70%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div class="pincard-wrapper">
|
||||
<div class="info-wrapper">
|
||||
<div class="info" @tap="gotoChat">
|
||||
<div class="info-left">
|
||||
<Avatar size="32" :account="props.msg.senderId"></Avatar>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="name">
|
||||
<Appellation
|
||||
:account="props.msg.senderId"
|
||||
:teamId="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
? to
|
||||
: ''
|
||||
"
|
||||
></Appellation>
|
||||
</div>
|
||||
<div class="createtime">
|
||||
{{ timeFormat() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Icon type="icon-More" @tap="handlePinMsg" />
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div
|
||||
class="msg-text"
|
||||
v-if="
|
||||
msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
>
|
||||
<MessageText :msg="props.msg"></MessageText>
|
||||
</div>
|
||||
<div class="file-wrapper">
|
||||
<div
|
||||
v-if="
|
||||
msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
|
||||
"
|
||||
>
|
||||
<div
|
||||
@tap="
|
||||
() => {
|
||||
handleImageTouch(props.msg.attachment.url)
|
||||
}
|
||||
"
|
||||
>
|
||||
<image
|
||||
class="msg-image"
|
||||
:lazy-load="true"
|
||||
mode="aspectFill"
|
||||
:src="imageUrl"
|
||||
></image>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="video-msg-wrapper"
|
||||
@tap="() => handleVideoTouch(props.msg)"
|
||||
>
|
||||
<div class="video-play-button">
|
||||
<div class="video-play-icon"></div>
|
||||
</div>
|
||||
<image
|
||||
class="msg-image"
|
||||
:lazy-load="true"
|
||||
mode="aspectFill"
|
||||
:src="videoFirstFrameDataUrl"
|
||||
></image>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="extra"
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
|
||||
"
|
||||
>
|
||||
<MessageFile :msg="props.msg" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
|
||||
"
|
||||
>
|
||||
<div class="audio-wrapper">
|
||||
<MessageAudio :msg="props.msg" mode="audio-in" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, withDefaults } from 'vue'
|
||||
import { stopAllAudio } from '@/utils/index'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import Appellation from '@/components/Appellation.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import MessageFile from './message-file.vue'
|
||||
import MessageAudio from './message-audio.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { customRedirectTo } from '@/utils/im/customNavigate'
|
||||
import MessageText from './message-text.vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msg: V2NIMMessageForUI
|
||||
handleUnPinMsg: any
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
/** 会话类型 */
|
||||
const conversationType =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
|
||||
props.msg.conversationId
|
||||
)
|
||||
|
||||
/** 会话对象 */
|
||||
const to = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
|
||||
props.msg.conversationId
|
||||
)
|
||||
|
||||
/** 调整至聊天页面 */
|
||||
const gotoChat = async () => {
|
||||
await uni.$UIKitStore.uiStore.selectConversation(props.msg.conversationId)
|
||||
customRedirectTo({
|
||||
url: '/pages/Chat/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 复制 */
|
||||
const handleCopy = () => {
|
||||
uni.setClipboardData({
|
||||
data: props.msg.text || '',
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('copySuccessText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: t('copyFailText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// pin 消息操作
|
||||
const handlePinMsg = () => {
|
||||
let itemList = [t('unpinText')]
|
||||
if (
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
) {
|
||||
itemList = itemList.concat([
|
||||
t('copyText'),
|
||||
t('forwardToTeamText'),
|
||||
t('forwardToFriendText'),
|
||||
])
|
||||
} else if (
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
|
||||
) {
|
||||
itemList = itemList.concat([
|
||||
t('forwardToTeamText'),
|
||||
t('forwardToFriendText'),
|
||||
])
|
||||
}
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success(data) {
|
||||
if (
|
||||
props.msg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
) {
|
||||
if (data.tapIndex === 0) {
|
||||
props.handleUnPinMsg()
|
||||
} else if (data.tapIndex === 1) {
|
||||
handleCopy()
|
||||
} else if (data.tapIndex === 2) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
|
||||
})
|
||||
} else if (data.tapIndex === 3) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
props.msg.messageType !==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
|
||||
) {
|
||||
if (data.tapIndex === 0) {
|
||||
props.handleUnPinMsg()
|
||||
} else if (data.tapIndex === 1) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
|
||||
})
|
||||
} else if (data.tapIndex === 2) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (data.tapIndex === 0) {
|
||||
props.handleUnPinMsg()
|
||||
}
|
||||
}
|
||||
},
|
||||
complete() {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 图片Url */
|
||||
const imageUrl = computed(() => {
|
||||
//@ts-ignore
|
||||
return props.msg?.attachment?.url || props.msg.attachment?.file
|
||||
})
|
||||
|
||||
// 获取视频首帧
|
||||
const videoFirstFrameDataUrl = computed(() => {
|
||||
//@ts-ignore
|
||||
const url = props.msg.attachment?.url
|
||||
return url ? `${url}${url.includes('?') ? '&' : '?'}vframe&offset=1` : ''
|
||||
})
|
||||
// 点击图片预览
|
||||
const handleImageTouch = (url: string) => {
|
||||
if (url) {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 点击视频播放
|
||||
const handleVideoTouch = (msg: V2NIMMessageForUI) => {
|
||||
stopAllAudio()
|
||||
//@ts-ignore
|
||||
const url = msg.attachment?.url
|
||||
if (url) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/video-play?videoUrl=${encodeURIComponent(url)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 时间格式化 */
|
||||
const isToday = (time: number) => {
|
||||
const createTime = new Date(time)
|
||||
const now = new Date()
|
||||
return (
|
||||
createTime.getFullYear() === now.getFullYear() &&
|
||||
createTime.getMonth() === now.getMonth() &&
|
||||
createTime.getDate() === now.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/** 时间格式化 */
|
||||
const isThisYear = (time: number) => {
|
||||
const createTime = new Date(time)
|
||||
const now = new Date()
|
||||
return createTime.getFullYear() === now.getFullYear()
|
||||
}
|
||||
|
||||
/** 时间格式化 */
|
||||
const timeFormat = () => {
|
||||
const createTime = props.msg.createTime
|
||||
if (isToday(createTime)) {
|
||||
return dayjs(createTime).format('HH:mm')
|
||||
} else if (isThisYear(createTime)) {
|
||||
return dayjs(createTime).format('MM月DD日 HH:mm')
|
||||
} else {
|
||||
return dayjs(createTime).format('YYYY年MM月DD日 HH:mm')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pincard-wrapper {
|
||||
background-color: #fff;
|
||||
margin: 0 20px;
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
.info-wrapper {
|
||||
display: flex;
|
||||
margin: 0 16px;
|
||||
padding: 16px 0 12px 0;
|
||||
border-bottom: 1px solid #e4e9f2;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.info {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
.info-left {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.info-right {
|
||||
font-size: 12px;
|
||||
.createtime {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-wrapper {
|
||||
padding: 12px 16px 16px 16px;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: break-spaces;
|
||||
.file-wrapper {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
.extra {
|
||||
width: 200%;
|
||||
}
|
||||
.msg-image {
|
||||
width: 100%;
|
||||
}
|
||||
.video-msg-wrapper {
|
||||
box-sizing: border-box;
|
||||
max-width: 360rpx;
|
||||
}
|
||||
.video-play-button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
z-index: 9;
|
||||
}
|
||||
.video-play-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-left: 18px solid #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-40%, -50%);
|
||||
}
|
||||
.audio-wrapper {
|
||||
width: fit-content;
|
||||
opacity: 1;
|
||||
background: #d6e5f6;
|
||||
overflow: hidden;
|
||||
padding: 10px 12px;
|
||||
border-radius: 0 8px 8px 8px;
|
||||
background-color: #e8eaed;
|
||||
}
|
||||
}
|
||||
.msg-text {
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
props.msg.sendingState ==
|
||||
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||||
"
|
||||
class="msg-read-wrapper"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
|
||||
p2pMsgReceiptVisible
|
||||
"
|
||||
>
|
||||
<div v-if="p2pMsgRotateDeg == 360" class="icon-read-wrapper">
|
||||
<Icon type="icon-read" :size="18"></Icon>
|
||||
</div>
|
||||
<div v-else class="sector">
|
||||
<span
|
||||
class="cover-1"
|
||||
:style="`transform: rotate(${p2pMsgRotateDeg}deg)`"
|
||||
></span>
|
||||
<span
|
||||
:class="p2pMsgRotateDeg >= 180 ? 'cover-2 cover-3' : 'cover-2'"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
|
||||
teamManagerVisible
|
||||
"
|
||||
>
|
||||
<div class="icon-read-wrapper" v-if="teamMsgRotateDeg == 360">
|
||||
<Icon type="icon-read" :size="18"></Icon>
|
||||
</div>
|
||||
<div v-else class="sector" @click="jumpToTeamMsgReadInfo">
|
||||
<span
|
||||
class="cover-1"
|
||||
:style="`transform: rotate(${teamMsgRotateDeg}deg)`"
|
||||
></span>
|
||||
<span
|
||||
:class="teamMsgRotateDeg >= 180 ? 'cover-2 cover-3' : 'cover-2'"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 消息已读未读组件 */
|
||||
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { autorun } from 'mobx'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msg: V2NIMMessageForUI
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
/** 是否需要显示群组消息已读未读,默认 false */
|
||||
const teamManagerVisible = uni.$UIKitStore.localOptions.teamMsgReceiptVisible
|
||||
|
||||
/** 是否需要显示 p2p 消息、p2p会话列表消息已读未读,默认 false */
|
||||
const p2pMsgReceiptVisible = uni.$UIKitStore.localOptions.p2pMsgReceiptVisible
|
||||
|
||||
/** 会话类型 */
|
||||
const conversationType =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
|
||||
props.msg.conversationId
|
||||
) as unknown as V2NIMConst.V2NIMConversationType
|
||||
|
||||
/** 单聊消息已读未读,用于UI变更 */
|
||||
const p2pMsgRotateDeg = ref(0)
|
||||
|
||||
/**是否是云端会话 */
|
||||
const enableV2CloudConversation =
|
||||
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
|
||||
|
||||
/** 设置单聊消息已读未读 */
|
||||
const setP2pMsgRotateDeg = () => {
|
||||
/**如果是单聊 */
|
||||
if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
|
||||
) {
|
||||
const conversation = enableV2CloudConversation
|
||||
? uni.$UIKitStore.conversationStore?.conversations.get(
|
||||
props.msg.conversationId
|
||||
)
|
||||
: uni.$UIKitStore.localConversationStore?.conversations.get(
|
||||
props.msg.conversationId
|
||||
)
|
||||
|
||||
p2pMsgRotateDeg.value =
|
||||
props?.msg?.createTime <= (conversation?.msgReceiptTime || 0) ? 360 : 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听单聊消息已读未读 */
|
||||
const p2pMsgReadWatch = autorun(() => {
|
||||
setP2pMsgRotateDeg()
|
||||
})
|
||||
|
||||
/** 跳转到已读未读详情 */
|
||||
const jumpToTeamMsgReadInfo = () => {
|
||||
if (
|
||||
uni.$UIKitStore.connectStore.connectStatus !==
|
||||
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
|
||||
) {
|
||||
uni.showToast({
|
||||
title: t('offlineText'),
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
// 跳转到消息已读未读详情页
|
||||
if (props?.msg?.messageClientId && props?.msg?.conversationId) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/message-read-info?messageClientId=${props.msg.messageClientId}&conversationId=${props.msg.conversationId}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 群消息已读未读,用于UI变更 */
|
||||
const teamMsgRotateDeg = computed(() => {
|
||||
if (
|
||||
conversationType ===
|
||||
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
|
||||
) {
|
||||
const percentage =
|
||||
(props?.msg?.yxRead || 0) /
|
||||
((props?.msg?.yxUnread || 0) + (props?.msg?.yxRead || 0)) || 0
|
||||
return percentage * 360
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setP2pMsgRotateDeg()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
p2pMsgReadWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-read-wrapper {
|
||||
align-self: flex-end;
|
||||
display: none;
|
||||
}
|
||||
.icon-read-wrapper {
|
||||
margin: 0px 10px 5px 0;
|
||||
}
|
||||
.sector {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 2px solid #4c84ff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 50%;
|
||||
margin: 0px 10px 0 0;
|
||||
|
||||
.cover-1,
|
||||
.cover-2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.cover-1 {
|
||||
background-color: #4c84ff;
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.cover-3 {
|
||||
right: 0;
|
||||
background-color: #4c84ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="props.replyMsg?.messageClientId" class="reply-msg-wrapper">
|
||||
<!-- replyMsg 不存在 说明回复的消息被删除或者撤回 -->
|
||||
<div v-if="!isReplyMsgExist">
|
||||
<span>{{ t('replyNotFindText') }}</span>
|
||||
</div>
|
||||
<div v-else class="reply-msg" @tap="showFullReplyMsg">
|
||||
<div class="reply-msg-name-wrapper">
|
||||
<div class="reply-msg-name-line">|</div>
|
||||
<div class="reply-msg-name-content">
|
||||
<Appellation
|
||||
:account="props.replyMsg?.senderId"
|
||||
:teamId="props.replyMsg?.receiverId"
|
||||
color="#929299"
|
||||
:fontSize="13"
|
||||
></Appellation>
|
||||
</div>
|
||||
<div class="reply-msg-name-to">:</div>
|
||||
</div>
|
||||
<message-one-line
|
||||
v-if="
|
||||
props.replyMsg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
:text="props.replyMsg.text"
|
||||
></message-one-line>
|
||||
<div
|
||||
v-else-if="
|
||||
props.replyMsg.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
|
||||
"
|
||||
class="other-msg-wrapper"
|
||||
>
|
||||
<uni-link
|
||||
v-if="!isHarmonyOs"
|
||||
:href="downloadUrl"
|
||||
:download="name"
|
||||
:showUnderLine="false"
|
||||
>
|
||||
{{ t('fileMsgTitleText') }}
|
||||
</uni-link>
|
||||
<span
|
||||
v-else
|
||||
class="other-msg-wrapper"
|
||||
@click="() => openInBrowser(downloadUrl)"
|
||||
>
|
||||
{{ t('fileMsgTitleText') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="other-msg-wrapper" v-else>
|
||||
{{ '[' + REPLY_MSG_TYPE_MAP[props.replyMsg.messageType] + ']' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 点击被回复的消息需要全屏显示 -->
|
||||
<div v-if="isFullScreen" class="reply-full-screen" @tap="closeFullReplyMsg">
|
||||
<!-- #ifdef MP -->
|
||||
<div class="reply-message-close-mp" @tap="closeFullReplyMsg">
|
||||
<Icon
|
||||
color="#929299"
|
||||
:iconStyle="{ fontWeight: '200' }"
|
||||
:size="18"
|
||||
type="icon-guanbi"
|
||||
/>
|
||||
</div>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP -->
|
||||
<div class="reply-message-close" @tap="closeFullReplyMsg">
|
||||
<Icon
|
||||
color="#929299"
|
||||
:iconStyle="{ fontWeight: '200' }"
|
||||
:size="18"
|
||||
type="icon-guanbi"
|
||||
/>
|
||||
</div>
|
||||
<!-- #endif -->
|
||||
<div
|
||||
v-if="
|
||||
props.replyMsg?.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
"
|
||||
class="reply-message-content"
|
||||
>
|
||||
<message-text :msg="replyMsg" :fontSize="22"></message-text>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
props.replyMsg?.messageType ==
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
|
||||
"
|
||||
class="msg-common"
|
||||
:style="{
|
||||
flexDirection: props.replyMsg?.isSelf ? 'row-reverse' : 'row',
|
||||
backgroundColor: props.replyMsg?.isSelf ? '#d6e5f6' : '#e8eaed',
|
||||
borderRadius: props.replyMsg?.isSelf
|
||||
? '8px 0px 8px 8px'
|
||||
: '0 8px 8px',
|
||||
}"
|
||||
@click.stop="() => {}"
|
||||
>
|
||||
<MessageAudio :msg="replyMsg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 回复消息组件 */
|
||||
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import MessageOneLine from '@/components/MessageOneLine.vue'
|
||||
import { ref, onMounted, computed, onUnmounted } from 'vue'
|
||||
import MessageText from './message-text.vue'
|
||||
// @ts-ignore
|
||||
import UniLink from '@/components/uni-components/uni-link/components/uni-link/uni-link.vue'
|
||||
import { REPLY_MSG_TYPE_MAP } from '@/utils/im/constants'
|
||||
import { events } from '@/utils/im/constants'
|
||||
import { isHarmonyOs, stopAllAudio } from '@/utils/im/index'
|
||||
import { autorun } from 'mobx'
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
import MessageAudio from './message-audio.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import Appellation from '@/components/Appellation.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ replyMsg: V2NIMMessageForUI | undefined }>(),
|
||||
{}
|
||||
)
|
||||
|
||||
/**是否全屏展示 */
|
||||
const isFullScreen = ref(false)
|
||||
|
||||
/** 回复对象 */
|
||||
const repliedTo = ref('')
|
||||
|
||||
//@ts-ignore
|
||||
const { name = '', url = '' } = props.replyMsg?.attachment || {}
|
||||
|
||||
/**下载地址 */
|
||||
const downloadUrl = computed(() => {
|
||||
//@ts-ignore
|
||||
const { name = '', url = '' } = props.replyMsg?.attachment || {}
|
||||
|
||||
return url + ((url as string).includes('?') ? '&' : '?') + `download=${name}`
|
||||
})
|
||||
|
||||
/**被回复消息是否存在 */
|
||||
const isReplyMsgExist = computed(() => {
|
||||
return props.replyMsg?.messageClientId !== 'noFind'
|
||||
})
|
||||
|
||||
/**回复消息昵称 */
|
||||
const repliedToWatch = autorun(() => {
|
||||
repliedTo.value = uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.replyMsg?.senderId as string,
|
||||
teamId: props.replyMsg?.receiverId,
|
||||
})
|
||||
})
|
||||
|
||||
/**全屏展示回复消息 */
|
||||
const showFullReplyMsg = () => {
|
||||
if (
|
||||
props.replyMsg?.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
|
||||
) {
|
||||
uni.previewImage({
|
||||
//@ts-ignore
|
||||
urls: [props.replyMsg?.attachment?.url as string],
|
||||
})
|
||||
} else if (
|
||||
props.replyMsg?.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
|
||||
) {
|
||||
isFullScreen.value = true
|
||||
uni.$emit(events.HANDLE_MOVE_THROUGH, true)
|
||||
} else if (
|
||||
props.replyMsg?.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
|
||||
) {
|
||||
//@ts-ignore
|
||||
const url = props.replyMsg?.attachment?.url
|
||||
stopAllAudio()
|
||||
if (url) {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/video-play?videoUrl=${encodeURIComponent(url)}`,
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
props.replyMsg?.messageType ===
|
||||
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
|
||||
) {
|
||||
isFullScreen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**点击全屏的回复消息,关闭全屏 */
|
||||
const closeFullReplyMsg = () => {
|
||||
isFullScreen.value = false
|
||||
stopAllAudio()
|
||||
uni.$emit(events.HANDLE_MOVE_THROUGH, false)
|
||||
}
|
||||
|
||||
/**复制下载链接 */
|
||||
const openInBrowser = (url: string) => {
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('openUrlText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
repliedTo.value = uni.$UIKitStore.uiStore.getAppellation({
|
||||
account: props.replyMsg?.senderId as string,
|
||||
teamId: props.replyMsg?.receiverId,
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
repliedToWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reply-msg-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #929299;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
|
||||
.reply-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
message-one-line {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.reply-msg-name-wrapper {
|
||||
margin-right: 5px;
|
||||
max-width: 125px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reply-msg-name-line {
|
||||
flex-basis: 0 0 3px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.reply-msg-name-to {
|
||||
flex-basis: 0 0 3px;
|
||||
}
|
||||
.reply-msg-name-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.reply-msg-content {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-full-screen {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
justify-content: center;
|
||||
touch-action: none;
|
||||
z-index: 999999999;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.reply-message-content {
|
||||
height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 30px 100px 30px;
|
||||
touch-action: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.other-msg-wrapper {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.reply-message-close {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
z-index: 999999;
|
||||
top: 60px;
|
||||
}
|
||||
|
||||
.reply-message-close-mp {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 100px;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.msg-common {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 16px;
|
||||
max-width: 360rpx;
|
||||
overflow: hidden;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div v-for="(item, index) in sliceMsgs">
|
||||
<text>{{ start + index }}</text>
|
||||
<messageItem
|
||||
:id="MSG_ID_FLAG + item.idClient"
|
||||
:scene="scene"
|
||||
:to="to"
|
||||
:msg="item"
|
||||
:key="item.idClient"
|
||||
:msg-index="start + index"
|
||||
>
|
||||
</messageItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import messageItem from './message-item.vue'
|
||||
import { computed } from '../../../utils/transformVue'
|
||||
|
||||
import { MSG_ID_FLAG } from '../../../utils/constants'
|
||||
import { caculateTimeago } from '../../../utils/date'
|
||||
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
|
||||
import { V2NIMConst } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/index'
|
||||
|
||||
const props = defineProps({
|
||||
msgs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
scene: {
|
||||
type: Object, // Assuming TMsgScene is a custom object type
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
start: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
end: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const sliceMsgs = computed(() => {
|
||||
const res: V2NIMMessage[] = []
|
||||
const msgs = props.msgs as V2NIMMessage[]
|
||||
const _slice = msgs.slice(props.start, props.end)
|
||||
_slice.forEach((item, index) => {
|
||||
const msgIndex = props.start + index
|
||||
// 如果两条消息间隔超过5分钟,插入一条自定义时间消息
|
||||
|
||||
if (
|
||||
msgIndex > 0 &&
|
||||
item.createTime - msgs[msgIndex - 1].createTime > 5 * 60 * 1000
|
||||
) {
|
||||
res.push({
|
||||
messageClientId: 'time-' + item.createTime,
|
||||
messageType: V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM,
|
||||
attachment: {
|
||||
type: 'time',
|
||||
|
||||
value: caculateTimeago(item.createTime),
|
||||
},
|
||||
sendingState:
|
||||
V2NIMConst.V2NIMMessageSendingState
|
||||
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED,
|
||||
})
|
||||
}
|
||||
|
||||
res.push(item)
|
||||
})
|
||||
return res.filter((item) => item.type !== 'notification')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="msg-text" :style="{ fontSize: (fontSize || 16) + 'px' }">
|
||||
<template v-for="item in textArr" :key="item.key">
|
||||
<template v-if="item.type === 'text'">
|
||||
<span class="msg-text">{{ item.value }}</span>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'Ait'">
|
||||
<text class="msg-text" :style="{ color: '#1861df' }">
|
||||
{{ ' ' + item.value + ' ' }}
|
||||
</text>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'emoji'">
|
||||
<Icon
|
||||
:type="EMOJI_ICON_MAP_CONFIG[item.value]"
|
||||
:size="fontSize || 22"
|
||||
:style="{ margin: '0 2px 2px 2px', verticalAlign: 'bottom' }"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'link'">
|
||||
<UniLink
|
||||
v-if="!isHarmonyOs"
|
||||
:href="item.value"
|
||||
:style="{ color: '#1861df', fontSize: (fontSize || 16) + 'px' }"
|
||||
:showUnderLine="false"
|
||||
>
|
||||
{{ item.value }}
|
||||
</UniLink>
|
||||
<span
|
||||
v-else
|
||||
:style="{ color: '#1861df', fontSize: (fontSize || 16) + 'px' }"
|
||||
@click="() => openInBrowser(item.value)"
|
||||
>
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**文本消息组件 */
|
||||
import Icon from '@/components/Icon.vue'
|
||||
// @ts-ignore
|
||||
import UniLink from '@/components/uni-components/uni-link/components/uni-link/uni-link.vue'
|
||||
import { parseText } from '@/utils/im/parseText'
|
||||
import { EMOJI_ICON_MAP_CONFIG } from '@/utils/im/emoji'
|
||||
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { isHarmonyOs } from '@/utils/im/index'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
msg: V2NIMMessageForUI
|
||||
fontSize?: number
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
/**解析文本 */
|
||||
const textArr = parseText(props.msg?.text || '', props.msg?.serverExtension)
|
||||
|
||||
/**unilink 不支持鸿蒙 故提示在浏览器打开链接 */
|
||||
const openInBrowser = (url: string) => {
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: t('openUrlText'),
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.msg-text {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
overflow-y: auto;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<!-- 样式兼容微信小程序 -->
|
||||
<div>
|
||||
<div
|
||||
class="nav-bar-wrapper"
|
||||
:style="{
|
||||
backgroundColor: backgroundColor || '#ffffff',
|
||||
backgroundImage: `url(${title})`,
|
||||
height: isWxApp ? '55px' : '40px',
|
||||
alignItems: isWxApp ? 'flex-end' : 'center',
|
||||
}"
|
||||
>
|
||||
<slot v-if="showLeft" name="left"></slot>
|
||||
<div v-else @tap="back">
|
||||
<Icon type="icon-zuojiantou" :size="22"></Icon>
|
||||
</div>
|
||||
<div class="title-container">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="subTitle" v-if="subTitle">{{ subTitle }}</div>
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isWxApp } from '@/utils/im/index'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
subTitle?: string
|
||||
backgroundColor?: string
|
||||
showLeft?: boolean
|
||||
}>(),
|
||||
{
|
||||
subTitle: '',
|
||||
backgroundColor: '',
|
||||
showLeft: true,
|
||||
}
|
||||
)
|
||||
|
||||
const back = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/common.scss';
|
||||
|
||||
.nav-bar-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--status-bar-height) 10px 5px 10px;
|
||||
z-index: 9999;
|
||||
|
||||
.title-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
max-width: 230px;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div>
|
||||
<NavBar :title="t('setText')" />
|
||||
<div class="p2p-set-container">
|
||||
<div class="p2p-set-card">
|
||||
<div class="p2p-set-item">
|
||||
<div class="p2p-set-my-info">
|
||||
<Avatar :account="account" />
|
||||
<div class="p2p-set-my-nick">{{ myNick }}</div>
|
||||
</div>
|
||||
<div class="member-add" @tap="addTeamMember">
|
||||
<Icon type="icon-tianjiaanniu" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p2p-set-card">
|
||||
<div class="p2p-set-item p2p-set-item-flex-sb" @tap="goPinInP2p">
|
||||
<div>{{ t('pinText') }}</div>
|
||||
<Icon iconClassName="more-icon" color="#999" type="icon-jiantou" />
|
||||
</div>
|
||||
<div class="p2p-set-item p2p-set-item-flex-sb">
|
||||
<div>{{ t('sessionMuteText') }}</div>
|
||||
<switch :checked="!isMute" @change="changeSessionMute" />
|
||||
</div>
|
||||
<div class="p2p-set-item p2p-set-item-flex-sb">
|
||||
<div>{{ t('stickTopText') }}</div>
|
||||
<switch :checked="isStickTop" @change="changeStickTopInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**单聊设置组件 */
|
||||
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import Avatar from '@/components/Avatar.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { autorun } from 'mobx'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { customNavigateTo } from '@/utils/im/customNavigate'
|
||||
import { V2NIMConst } from '@/utils/im/nim'
|
||||
|
||||
const myNick = ref('')
|
||||
const conversation = ref()
|
||||
const isMute = ref(false)
|
||||
const isStickTop = ref(false)
|
||||
const account = ref('')
|
||||
const conversationId = ref('')
|
||||
|
||||
let p2pSetWatch: () => void
|
||||
|
||||
/**是否是云端会话 */
|
||||
const enableV2CloudConversation =
|
||||
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
|
||||
|
||||
onLoad((option) => {
|
||||
const _account = option?.id
|
||||
account.value = _account
|
||||
|
||||
const _conversationId =
|
||||
uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(_account)
|
||||
conversationId.value = _conversationId
|
||||
|
||||
p2pSetWatch = autorun(() => {
|
||||
conversation.value = enableV2CloudConversation
|
||||
? uni.$UIKitStore.conversationStore?.conversations.get(_conversationId)
|
||||
: uni.$UIKitStore.localConversationStore?.conversations.get(
|
||||
_conversationId
|
||||
)
|
||||
|
||||
myNick.value = uni.$UIKitStore.uiStore.getAppellation({ account: _account })
|
||||
|
||||
isMute.value = uni.$UIKitStore.relationStore.mutes.includes(_account)
|
||||
|
||||
isStickTop.value = !!conversation.value?.stickTop
|
||||
})
|
||||
})
|
||||
|
||||
/**添加群成员 */
|
||||
const addTeamMember = () => {
|
||||
const to = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
|
||||
conversationId.value
|
||||
)
|
||||
customNavigateTo({
|
||||
url: `/pages/Team/team-create/index?p2pConversationId=${to}`,
|
||||
})
|
||||
}
|
||||
|
||||
/**跳转至pin页面 */
|
||||
const goPinInP2p = () => {
|
||||
customNavigateTo({
|
||||
url: `/pages/Chat/message/pin-list?conversationId=${conversationId.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
/**修改会话免打扰 */
|
||||
const changeSessionMute = async (e: any) => {
|
||||
const checked = !e.detail.value
|
||||
try {
|
||||
await uni.$UIKitStore.relationStore.setP2PMessageMuteModeActive(
|
||||
account.value,
|
||||
checked
|
||||
? V2NIMConst.V2NIMP2PMessageMuteMode.V2NIM_P2P_MESSAGE_MUTE_MODE_ON
|
||||
: V2NIMConst.V2NIMP2PMessageMuteMode.V2NIM_P2P_MESSAGE_MUTE_MODE_OFF
|
||||
)
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: checked ? t('sessionMuteFailText') : t('sessionUnMuteFailText'),
|
||||
icon: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**修改置顶 */
|
||||
const changeStickTopInfo = async (e: any) => {
|
||||
const checked = e.detail.value
|
||||
try {
|
||||
if (enableV2CloudConversation) {
|
||||
await uni.$UIKitStore.conversationStore?.stickTopConversationActive(
|
||||
conversationId.value,
|
||||
checked
|
||||
)
|
||||
} else {
|
||||
await uni.$UIKitStore.localConversationStore?.stickTopConversationActive(
|
||||
conversationId.value,
|
||||
checked
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: checked ? t('addStickTopFailText') : t('deleteStickTopFailText'),
|
||||
icon: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
p2pSetWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/common.scss';
|
||||
|
||||
page {
|
||||
padding-top: var(--status-bar-height);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p2p-set-container {
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
background-color: #eff1f4;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.p2p-set-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding-left: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.p2p-set-button {
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
color: #e6605c;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.p2p-set-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 10px 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1rpx solid #f5f8fc;
|
||||
}
|
||||
}
|
||||
|
||||
.p2p-set-item-flex-sb {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.p2p-set-my-info {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p2p-set-my-nick {
|
||||
margin-top: 5px;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
max-width: 70px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
margin: 0 16px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.p2p-info-item {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p2p-info-title {
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //溢出用省略号显示
|
||||
white-space: nowrap; //溢出不换行
|
||||
}
|
||||
}
|
||||
|
||||
.p2p-members-item {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.p2p-members-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p2p-members-info {
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
|
||||
.p2p-info-subtitle {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.member-list {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
margin-right: 30px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-add {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 100%;
|
||||
border: 1px dashed #999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div :class="`wrapper ${pinInfos.length === 0 ? 'bg-white' : ''}`">
|
||||
<NavBar :title="t('pinText')">
|
||||
<template v-slot:left>
|
||||
<div class="nav-bar-text" @tap="back">{{ t('pinText') }}</div>
|
||||
</template>
|
||||
</NavBar>
|
||||
<div class="pinCard-item-wrapper">
|
||||
<Empty v-if="pinInfos.length === 0" :text="t('noPinListText')" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-for="(item, index) in pinInfos"
|
||||
:key="item.message.messageClientId"
|
||||
>
|
||||
<PinCard
|
||||
:msg="item.message"
|
||||
:index="index"
|
||||
:key="item.message.messageClientId"
|
||||
:handleUnPinMsg="handleUnPinMsg(item.message)"
|
||||
>
|
||||
</PinCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { autorun } from 'mobx'
|
||||
import { onUnmounted, ref } from '@/utils/im/transformVue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import PinCard from '@/components/PinCard.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { getUniPlatform } from '@/utils/index'
|
||||
import { deepClone } from '@/utils/index'
|
||||
import { V2NIMTeamMember } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMTeamService'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
const inputValue = ref('')
|
||||
const showClearIcon = ref(false)
|
||||
const myMemberInfo = ref<V2NIMTeamMember>()
|
||||
let teamId = ''
|
||||
let conversationId = ''
|
||||
let pinInfos = ref([])
|
||||
let uninstallTeamMemberWatch = () => {}
|
||||
|
||||
onLoad((option) => {
|
||||
conversationId = option?.conversationId
|
||||
getPinnedMessageList()
|
||||
})
|
||||
const getPinnedMessageList = () => {
|
||||
uni.$UIKitStore.msgStore
|
||||
.getPinnedMessageListActive(conversationId)
|
||||
.then((data) => {
|
||||
pinInfos.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const handleUnPinMsg = (msg) => {
|
||||
return () => {
|
||||
// 不用进行 catch 处理,因为 store 里面的 pin 相关方法处理过了,并且会将错误日志输出到控制台
|
||||
return uni.$UIKitStore.msgStore
|
||||
.unpinMessageActive(msg)
|
||||
.then(() => {
|
||||
return getPinnedMessageList()
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({
|
||||
title: t('unpinFailedText'),
|
||||
icon: 'none',
|
||||
duration: 1000,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
uninstallTeamMemberWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/common.scss';
|
||||
|
||||
page {
|
||||
padding-top: var(--status-bar-height);
|
||||
height: 100vh;
|
||||
background-color: rgb(245, 246, 247);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
background-color: rgb(245, 246, 247);
|
||||
|
||||
.nav-bar-text {
|
||||
color: rgb(20, 146, 209);
|
||||
}
|
||||
}
|
||||
.bg-white {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div :class="`wrapper ${pinInfos.length === 0 ? 'bg-white' : ''}`">
|
||||
<NavBar :title="t('pinText')">
|
||||
<template v-slot:left>
|
||||
<div class="nav-bar-text" @tap="back">{{ t('pinText') }}</div>
|
||||
</template>
|
||||
</NavBar>
|
||||
<div class="pinCard-item-wrapper">
|
||||
<Empty v-if="pinInfos.length === 0" :text="t('noPinListText')" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-for="(item, index) in pinInfos"
|
||||
:key="item.message.messageClientId"
|
||||
>
|
||||
<PinCard
|
||||
:msg="item.message"
|
||||
:index="index"
|
||||
:key="item.message.messageClientId"
|
||||
:handleUnPinMsg="handleUnPinMsg(item.message)"
|
||||
>
|
||||
</PinCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**pin 消息组件 */
|
||||
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { autorun } from 'mobx'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import PinCard from './message-pin-card.vue'
|
||||
import { t } from '@/utils/im/'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
|
||||
import {
|
||||
V2NIMMessage,
|
||||
V2NIMMessagePin,
|
||||
} from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
|
||||
|
||||
export type PinInfo = V2NIMMessagePin & {
|
||||
operatorId?: string
|
||||
pinState: number
|
||||
message?: V2NIMMessage
|
||||
}
|
||||
|
||||
let conversationId = ''
|
||||
let pinInfos = ref<PinInfo[]>([])
|
||||
let pinInfosWatch = () => {}
|
||||
|
||||
onLoad((option) => {
|
||||
conversationId = option?.conversationId
|
||||
pinInfosWatch = autorun(() => {
|
||||
const curPinMsgsMap =
|
||||
uni.$UIKitStore.msgStore.pinMsgs.map.get(conversationId)
|
||||
//@ts-ignore
|
||||
pinInfos.value = [...curPinMsgsMap.values()]
|
||||
.filter((pinInfo) => pinInfo.pinState > 0 && pinInfo.message)
|
||||
.sort((a, b) => b.message!.createTime - a.message!.createTime)
|
||||
})
|
||||
})
|
||||
|
||||
/**取消pin */
|
||||
const handleUnPinMsg = (msg: V2NIMMessage) => {
|
||||
return () => {
|
||||
// 不用进行 catch 处理,因为 store 里面的 pin 相关方法处理过了,并且会将错误日志输出到控制台
|
||||
return uni.$UIKitStore.msgStore
|
||||
.unpinMessageActive(msg)
|
||||
.catch((err: any) => {
|
||||
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
|
||||
uni.showToast({
|
||||
title: t(`${err.code}`),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('unpinFailedText'),
|
||||
icon: 'error',
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
pinInfosWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/common.scss';
|
||||
|
||||
page {
|
||||
padding-top: var(--status-bar-height);
|
||||
background-color: rgb(245, 246, 247);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 30px;
|
||||
background-color: rgb(245, 246, 247);
|
||||
min-height: 100vh;
|
||||
.nav-bar-text {
|
||||
color: rgb(20, 146, 209);
|
||||
}
|
||||
}
|
||||
.bg-white {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="voice-panel-wrapper">
|
||||
<div class="audio-remind-text">{{ t('audioRemindText') }}</div>
|
||||
<div class="voice-panel">
|
||||
<div
|
||||
class="voice-panel-circel"
|
||||
@touchstart="onStartRecord"
|
||||
@touchend="onStopRecord"
|
||||
>
|
||||
<div class="img-mask"></div>
|
||||
<Icon :width="24" :height="30" type="audio-btn"></Icon>
|
||||
</div>
|
||||
<div
|
||||
:style="{ display: recordState == 'stop' ? 'none' : 'block' }"
|
||||
class="big-circle"
|
||||
></div>
|
||||
<div
|
||||
:style="{ display: recordState == 'stop' ? 'none' : 'block' }"
|
||||
class="small-circle"
|
||||
></div>
|
||||
</div>
|
||||
<div class="audio-btn-text">{{ t('audioBtnText') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**语音消息面板组件 */
|
||||
|
||||
import { ref } from 'vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
import { isWxApp, stopAllAudio } from '@/utils/im/index'
|
||||
|
||||
const $emit = defineEmits(['handleSendAudioMsg'])
|
||||
|
||||
/**录音实例 */
|
||||
const recorderManager = uni.getRecorderManager()
|
||||
|
||||
let startRecordStamp = 0
|
||||
|
||||
/**录音状态 */
|
||||
const recordState = ref('stop')
|
||||
|
||||
/**开始录音 */
|
||||
const onStartRecord = () => {
|
||||
console.log('开始录音')
|
||||
try {
|
||||
stopAllAudio()
|
||||
recorderManager.start({
|
||||
format: 'mp3',
|
||||
duration: 60000,
|
||||
sampleRate: 44100,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 192000,
|
||||
frameSize: 50,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('录音失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**结束录音 */
|
||||
const onStopRecord = () => {
|
||||
console.log('结束录音')
|
||||
recordState.value = 'stop'
|
||||
recorderManager.stop()
|
||||
}
|
||||
|
||||
/** 开始录音 */
|
||||
recorderManager.onStart((res) => {
|
||||
console.log('recorder start' + JSON.stringify(res))
|
||||
console.log('recorder start')
|
||||
recordState.value = 'recording'
|
||||
startRecordStamp = new Date().getTime()
|
||||
})
|
||||
|
||||
/** 结束录音 */
|
||||
recorderManager.onStop((res) => {
|
||||
console.log('recorder stop' + JSON.stringify(res))
|
||||
recordState.value = 'stop'
|
||||
const duration = new Date().getTime() - startRecordStamp
|
||||
if (duration < 1000) {
|
||||
uni.showToast({
|
||||
title: '录音时间太短',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
$emit('handleSendAudioMsg', res.tempFilePath, duration)
|
||||
})
|
||||
|
||||
/** 录音出错 */
|
||||
recorderManager.onError((res) => {
|
||||
console.log('recorder error', res)
|
||||
recordState.value = 'stop'
|
||||
if (!isWxApp) {
|
||||
uni.showToast({
|
||||
title: t('audioErrorText'),
|
||||
icon: 'none',
|
||||
})
|
||||
} else {
|
||||
if (res.errMsg == 'operateRecorder:fail auth deny') {
|
||||
uni.showToast({
|
||||
title: t('audioErrorText'),
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voice-panel-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.voice-panel {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.audio-remind-text {
|
||||
margin-top: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.voice-panel-circel {
|
||||
z-index: 3;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(163deg, #4883ea 11%, #2561c9 121%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.big-circle {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 230px;
|
||||
height: 230px;
|
||||
left: 20%;
|
||||
top: 10%;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #cce6f8 38.14%;
|
||||
animation: circleSmall 1.5s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.small-circle {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 50%;
|
||||
background: #a0d2f0;
|
||||
top: 28%;
|
||||
left: 33%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: circleSmall 1.5s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes circleSmall {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.3);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.img-mask {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.audio-btn-text {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="video-wrapper">
|
||||
<NavBar :title="t('videoPlayText')" :showLeft="false"></NavBar>/>
|
||||
<div class="video-box">
|
||||
<video
|
||||
v-if="show"
|
||||
class="video"
|
||||
:src="videoUrl"
|
||||
id="videoEle"
|
||||
controls
|
||||
autoplay
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/** 视频播放界面 */
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReady } from '@dcloudio/uni-app'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import { t } from '@/utils/im/i18n'
|
||||
|
||||
/** 视频地址 */
|
||||
const videoUrl = ref()
|
||||
/** 是否显示视频 */
|
||||
const show = ref(false)
|
||||
|
||||
onLoad((option: any) => {
|
||||
videoUrl.value = decodeURIComponent(option.videoUrl)
|
||||
show.value = true
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
show.value = true
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.video-wrapper {
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-box {
|
||||
width: 100vw;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
.video {
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav">
|
||||
<view class="back" @click="onBack">
|
||||
<uni-icons type="left" color="#8B2316" size="24"></uni-icons>
|
||||
</view>
|
||||
<view class="title">群发消息</view>
|
||||
<view class="right"></view>
|
||||
</view>
|
||||
|
||||
<!-- 时间展示 -->
|
||||
<view class="time-row">{{ currentTime }}</view>
|
||||
|
||||
<!-- 卡片区域 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="label">{{ patientCount }}位患者:</text>
|
||||
<view class="close" @click="onClear">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="line">{{ patientName }}</view>
|
||||
<view class="line phone">{{ patientPhone }}</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="btn-outline" @click="onResend">再发一条</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 占位滚动区域 -->
|
||||
<view class="spacer" />
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="btn-primary" @click="onSendGroup">群发消息</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const currentTime = ref('')
|
||||
const patientCount = ref(1)
|
||||
const patientName = ref('测试')
|
||||
const patientPhone = ref('155555555')
|
||||
|
||||
const pad = (n) => (n < 10 ? `0${n}` : `${n}`)
|
||||
const updateTime = () => {
|
||||
const d = new Date()
|
||||
currentTime.value = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
// 每秒刷新一次时间(如不需要可删除)
|
||||
setInterval(updateTime, 1000)
|
||||
})
|
||||
|
||||
const onBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
const onClear = () => {
|
||||
// 清空选择的患者(占位)
|
||||
patientCount.value = 0
|
||||
patientName.value = ''
|
||||
patientPhone.value = ''
|
||||
}
|
||||
const onResend = () => {
|
||||
uni.showToast({ title: '已触发再发一条', icon: 'none' })
|
||||
}
|
||||
const onSendGroup = () => {
|
||||
uni.showToast({ title: '已触发群发', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$page-bg: #f5f5f5;
|
||||
$brand: #8B2316;
|
||||
$brand-deep: #8B2316;
|
||||
$primary: #00cbc0;
|
||||
$red: #D32F2F;
|
||||
|
||||
.page {
|
||||
background: $page-bg;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||
.back { width: 80rpx; display: flex; align-items: center; }
|
||||
.title { flex: 1; text-align: center; font-size: 34rpx; color: $red; font-weight: 600; }
|
||||
.right { width: 80rpx; }
|
||||
}
|
||||
|
||||
.time-row {
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
padding: 32rpx 0;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
.card-header {
|
||||
position: relative;
|
||||
padding: 28rpx 28rpx 12rpx 28rpx;
|
||||
.border { height: 2rpx; background: #eee; }
|
||||
.label { font-size: 32rpx; color: #333; }
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-bottom-left-radius: 100rpx;
|
||||
background: #8B2316;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
text { color: #fff; font-size: 40rpx; font-weight: 500; }
|
||||
}
|
||||
}
|
||||
.card-body {
|
||||
padding: 20rpx 28rpx 8rpx 28rpx;
|
||||
.line { font-size: 32rpx; color: #333; padding: 16rpx 0; }
|
||||
.phone { font-size: 36rpx; }
|
||||
}
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16rpx 28rpx 28rpx 28rpx;
|
||||
.btn-outline {
|
||||
border: 2rpx solid $red;
|
||||
color: $red;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spacer { height: 200rpx; }
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
background: #00cbc0;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user