更新
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
.panel-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.panel-item {
|
||||
background-color: #F9FAFC;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.panel-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.panel-text {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #00000066;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="panel-box">
|
||||
<div class="panel-item" @click="handleClick">
|
||||
<image class="panel-icon" :src="CameraPickerIcon"></image>
|
||||
</div>
|
||||
<text class="panel-text">拍照</text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useMessageInputState } from '../../../index';
|
||||
import CameraPickerIcon from '../../../assets/chat/camera-picker.svg'
|
||||
import { MessageContentType } from '../../../constants/chat'
|
||||
|
||||
const { sendMessage } = useMessageInputState();
|
||||
const emit = defineEmits(['closePanel']);
|
||||
|
||||
const handleClick = () => {
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image', 'video'],
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['camera'],
|
||||
camera: 'back',
|
||||
success: function (res: any) {
|
||||
emit('closePanel');
|
||||
if (res.type === MessageContentType.IMAGE) {
|
||||
sendMessage({
|
||||
type: MessageContentType.IMAGE,
|
||||
content: res
|
||||
})
|
||||
}
|
||||
if (res.type === MessageContentType.VIDEO) {
|
||||
sendMessage({
|
||||
type: MessageContentType.VIDEO,
|
||||
content: res
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: function (err: any) {
|
||||
console.error('chooseMedia failed:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './AttachmentPicker.module.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="panel-box">
|
||||
<div class="panel-item" @click="handleClick">
|
||||
<image class="panel-icon" :src="PhotoPickerIcon"></image>
|
||||
</div>
|
||||
<text class="panel-text">图片</text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useMessageInputState } from '../../../states/MessageInputState';
|
||||
import PhotoPickerIcon from '../../../assets/chat/photo-picker.svg'
|
||||
import { MessageContentType } from '../../../constants/chat'
|
||||
|
||||
const { sendMessage } = useMessageInputState();
|
||||
const emit = defineEmits(['closePanel']);
|
||||
|
||||
const handleClick = () => {
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image', 'video'],
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album'],
|
||||
success: function (res: any) {
|
||||
emit('closePanel');
|
||||
if (res.type === MessageContentType.IMAGE) {
|
||||
sendMessage({
|
||||
type: MessageContentType.IMAGE,
|
||||
content: res
|
||||
})
|
||||
}
|
||||
if (res.type === MessageContentType.VIDEO) {
|
||||
sendMessage({
|
||||
type: MessageContentType.VIDEO,
|
||||
content: res
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: function (err: any) {
|
||||
console.error('chooseMedia failed:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './AttachmentPicker.module.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="panel-box">
|
||||
<div class="panel-item" @click="handleCall">
|
||||
<image class="panel-icon" :src="CallVideoIcon"></image>
|
||||
</div>
|
||||
<text class="panel-text">视频通话</text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useConversationListState } from '../../../states/ConversationListState';
|
||||
import { removeC2C } from '../../../utils'
|
||||
import CallVideoIcon from '../../../assets/chat/call-video.svg'
|
||||
import { TUIBridge } from '../../../TUIBridge';
|
||||
import { EVENT } from '../../../constants/event';
|
||||
|
||||
const emit = defineEmits(['closePanel', 'showUserPicker']);
|
||||
const { activeConversation } = useConversationListState();
|
||||
|
||||
const handleCall = () => {
|
||||
emit('closePanel');
|
||||
|
||||
if (activeConversation.value?.type === 'GROUP') {
|
||||
// 群聊:触发显示用户选择器事件
|
||||
emit('showUserPicker', { type: 2 });
|
||||
} else {
|
||||
// 单聊:直接呼叫
|
||||
const userID = removeC2C(activeConversation.value.conversationID);
|
||||
TUIBridge.notifyEvent({
|
||||
eventName: EVENT.ON_CALLS,
|
||||
params: {
|
||||
userIDList: [userID],
|
||||
type: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './AttachmentPicker.module.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="panel-box">
|
||||
<div class="panel-item" @click="handleCall">
|
||||
<image class="panel-icon" :src="CallVoiceIcon"></image>
|
||||
</div>
|
||||
<text class="panel-text">语音通话</text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useConversationListState } from '../../../states/ConversationListState';
|
||||
import { removeC2C } from '../../../utils'
|
||||
import CallVoiceIcon from '../../../assets/chat/call-voice.svg'
|
||||
import { TUIBridge } from '../../../TUIBridge';
|
||||
import { EVENT } from '../../../constants/event';
|
||||
|
||||
const emit = defineEmits(['closePanel', 'showUserPicker']);
|
||||
const { activeConversation } = useConversationListState();
|
||||
|
||||
const handleCall = () => {
|
||||
emit('closePanel');
|
||||
|
||||
if (activeConversation.value?.type === 'GROUP') {
|
||||
// 群聊:触发显示用户选择器事件
|
||||
emit('showUserPicker', { type: 1 });
|
||||
} else {
|
||||
// 单聊:直接呼叫
|
||||
const userID = removeC2C(activeConversation.value.conversationID);
|
||||
TUIBridge.notifyEvent({
|
||||
eventName: EVENT.ON_CALLS,
|
||||
params: {
|
||||
userIDList: [userID],
|
||||
type: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './AttachmentPicker.module.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<!-- 消息输入容器-->
|
||||
<div class="message-input-container" :style="{ paddingBottom: keyboardHeight > 0 ? keyboardHeight + 'px' : '' }">
|
||||
<!-- 正常状态:可以发送消息 -->
|
||||
<view class="input-container" v-if="canSendMessage">
|
||||
<!-- 消息输入框 -->
|
||||
<input class="message-input" type="text" :value="inputRawValue" confirm-type="send" @confirm="sendInputMessage"
|
||||
@input="updateRawValue($event.target.value)" :adjust-position="false"
|
||||
:style="{ borderColor: inputRawValue ? '#006eff' : 'transparent' }" />
|
||||
|
||||
<!-- 更多功能按钮(当输入框为空时显示) -->
|
||||
<view class="icon-btn" v-if="!inputRawValue" @click="toggleMorePanel">
|
||||
<image :src="MorePanelIcon"></image>
|
||||
</view>
|
||||
|
||||
<!-- 发送按钮(当输入框有内容时显示) -->
|
||||
<view class="send-btn" v-if="inputRawValue" @click="sendInputMessage">
|
||||
<text>发送</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出群聊状态:显示提示信息 -->
|
||||
<view class="input-container disabled-container" v-else>
|
||||
<view class="disabled-message">
|
||||
<text class="disabled-text">你已退出群聊,无法发送消息</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多功能面板 -->
|
||||
<view class="more-panel" v-if="showMorePanel" @touchmove.stop.prevent>
|
||||
<view class="panel-content">
|
||||
<ImagePicker @closePanel="closePanel" />
|
||||
<PhotoPicker @closePanel="closePanel" />
|
||||
<VideoCallPicker @closePanel="closePanel" @showUserPicker="handleShowUserPicker" />
|
||||
<VoiceCallPicker @closePanel="closePanel" @showUserPicker="handleShowUserPicker" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户选择器弹窗 -->
|
||||
<view v-if="showUserPicker" class="user-picker-modal">
|
||||
<view class="modal-mask" @click="closeUserPicker"></view>
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择通话用户</text>
|
||||
<view class="close-btn" @click="closeUserPicker">×</view>
|
||||
</view>
|
||||
<UserPicker :maxCount="1" :dataSource="filteredGroupMembers" :enableDelete="false" :inModal="true"
|
||||
@confirm="handleUserSelect" />
|
||||
</view>
|
||||
</view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
virtualHost: true,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useMessageInputState } from '../../states/MessageInputState';
|
||||
import { useConversationListState } from '../../states/ConversationListState';
|
||||
import { useGroupSettingState } from '../../states/GroupSettingState';
|
||||
import VideoCallPicker from './AttachmentPicker/VideoCallPicker.vue';
|
||||
import VoiceCallPicker from './AttachmentPicker/voiceCallPicker.vue';
|
||||
import ImagePicker from './AttachmentPicker/CameraPicker.vue';
|
||||
import PhotoPicker from './AttachmentPicker/PhotoPicker.vue';
|
||||
import UserPicker from '../UserPicker/UserPicker.vue';
|
||||
import MorePanelIcon from '../../assets/chat/more-panel.svg';
|
||||
import { TUIBridge } from '../../TUIBridge';
|
||||
import { EVENT } from '../../constants/event';
|
||||
import { MessageContentType } from '../../constants/chat'
|
||||
|
||||
const { inputRawValue, updateRawValue, sendMessage } = useMessageInputState();
|
||||
const { activeConversation } = useConversationListState();
|
||||
const { allMembers, isInGroup, currentUserID, getGroupMemberList } = useGroupSettingState();
|
||||
|
||||
const showMorePanel = ref(false);
|
||||
const showUserPicker = ref(false);
|
||||
const currentCallType = ref(1);
|
||||
const keyboardHeight = ref(0);
|
||||
|
||||
// 判断是否可以发送消息
|
||||
const canSendMessage = computed(() => {
|
||||
// 如果不是群聊,可以发送消息
|
||||
if (activeConversation.value?.type !== 'GROUP') {
|
||||
return true;
|
||||
}
|
||||
// 如果是群聊,检查是否在群里
|
||||
return isInGroup.value !== false;
|
||||
});
|
||||
|
||||
// 过滤掉自己的群成员列表
|
||||
const filteredGroupMembers = computed(() => {
|
||||
return (allMembers.value || []).filter(member => member.userID !== currentUserID.value);
|
||||
});
|
||||
|
||||
const toggleMorePanel = () => {
|
||||
showMorePanel.value = !showMorePanel.value;
|
||||
};
|
||||
|
||||
const closePanel = () => {
|
||||
showMorePanel.value = false;
|
||||
};
|
||||
|
||||
const sendInputMessage = async () => {
|
||||
if (!inputRawValue.value) return;
|
||||
sendMessage({
|
||||
type: MessageContentType.TEXT,
|
||||
content: inputRawValue.value
|
||||
});
|
||||
inputRawValue.value = '';
|
||||
};
|
||||
|
||||
const handleShowUserPicker = ({ type }) => {
|
||||
currentCallType.value = type;
|
||||
showUserPicker.value = true;
|
||||
};
|
||||
|
||||
const handleUserSelect = (selectedUserIDs: string[]) => {
|
||||
if (selectedUserIDs.length > 0) {
|
||||
TUIBridge.notifyEvent({
|
||||
eventName: EVENT.ON_CALLS,
|
||||
params: {
|
||||
userIDList: selectedUserIDs,
|
||||
type: currentCallType.value,
|
||||
// groupID: activeConversation.value.groupID,
|
||||
},
|
||||
});
|
||||
}
|
||||
closeUserPicker();
|
||||
};
|
||||
|
||||
const closeUserPicker = () => {
|
||||
showUserPicker.value = false;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (activeConversation.value?.type === 'GROUP') {
|
||||
await getGroupMemberList({ count: 100 });
|
||||
}
|
||||
// Listen for keyboard height changes
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
keyboardHeight.value = res.height;
|
||||
if (res.height > 0) {
|
||||
// Close more panel when keyboard pops up
|
||||
showMorePanel.value = false;
|
||||
uni.$emit('TUIChat:keyboardHeightChange', res.height);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.offKeyboardHeightChange();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-input-container {
|
||||
position: relative;
|
||||
/* iOS安全区域适配 - 稍微增加间距 */
|
||||
padding-bottom: calc(constant(safe-area-inset-bottom) * 0.4);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) * 0.4);
|
||||
/* 添加背景色覆盖安全区域 */
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-color: #FFFFFF;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
/* 移除额外间距,只保留外层容器的安全区域适配 */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
padding: 0 15px;
|
||||
background-color: #F0F2F7;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
border: 1px solid transparent;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.disabled-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f8f9fa;
|
||||
/* iOS安全区域适配 - 稍微增加间距 */
|
||||
padding-bottom: calc(constant(safe-area-inset-bottom) * 0.55);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) * 0.55);
|
||||
}
|
||||
|
||||
.disabled-message {
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.disabled-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
min-width: 60px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #006eff;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.more-panel {
|
||||
width: 100%;
|
||||
height: 125px;
|
||||
background-color: #fff;
|
||||
z-index: 99;
|
||||
/* iOS安全区域适配 - 稍微增加间距 */
|
||||
padding-bottom: calc(constant(safe-area-inset-bottom) * 0.55);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) * 0.55);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
height: 100%;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border-top: 1px solid #eee;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-picker-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 95%;
|
||||
max-width: 500px;
|
||||
height: 90vh;
|
||||
max-height: 800px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user