IM/frontend/web/src/views/messages/MessageContent.vue

290 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<section class="chat-panel">
<header class="chat-header">
<span class="title">{{ conversationInfo?.targetName || '未选择会话' }}</span>
<div class="actions">
<button @click="startCall('video')" v-html="feather.icons['video'].toSvg({width:16, height: 16})"></button>
<button @click="startCall('voice')" v-html="feather.icons['phone'].toSvg({width:16, height: 16})"></button>
</div>
</header>
<div class="chat-history" ref="historyRef">
<div v-for="m in chatStore.messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
<img :src="m.senderId == myInfo.id ? (myInfo?.avatar || defaultAvatar) : defaultAvatar" class="avatar-chat" />
<div class="msg-content">
<div class="bubble">
<div v-if="m.type === 'Text'">{{ m.content }}</div>
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
<div class="status" v-if="m.senderId == myInfo.id">
<i v-if="m.isFail" style="color: red;" v-html="feather.icons['alert-circle'].toSvg({width:18, height: 18})"></i>
<i v-if="m.isLoading" class="loaderIcon" v-html="feather.icons['loader'].toSvg({width:18, height: 18})"></i>
</div>
</div>
<span class="msg-time">{{ formatDate(m.timeStamp) }}</span>
</div>
</div>
</div>
<footer class="chat-footer">
<div class="toolbar">
<button class="tool-btn" @click="toggleEmoji" v-html="feather.icons['smile'].toSvg({width:18, height: 18})">
</button>
<label class="tool-btn">
<i v-html="feather.icons['file'].toSvg({width:18, height: 18})"></i>
<input type="file" hidden @change="handleFile" />
</label>
</div>
<textarea
v-model="input"
placeholder="请输入消息..."
@keydown.enter.exact.prevent="sendText"
></textarea>
<div class="send-row">
<button class="send-btn" :disabled="!input.trim()" @click="sendText">发送(S)</button>
</div>
</footer>
</section>
</template>
<script setup>
import { ref, computed, nextTick, onMounted, watch } from 'vue';
import { useAuthStore } from '@/stores/auth';
import defaultAvatar from '@/assets/default_avatar.png';
import { messageService } from '@/services/message';
import { formatDate } from '@/utils/formatDate';
import { useChatStore } from '@/stores/chat';
import { generateSessionId } from '@/utils/sessionIdTools';
import { useSignalRStore } from '@/stores/signalr';
import { useConversationStore } from '@/stores/conversation';
import feather from 'feather-icons';
import { onBeforeRouteUpdate } from 'vue-router';
const props = defineProps({
id:{
type: String,
required:true
}
})
const chatStore = useChatStore();
const signalRStore = useSignalRStore();
const conversationStore = useConversationStore();
const input = ref(''); // 输入框内容
const historyRef = ref(null); // 绑定 DOM 用于滚动
const myInfo = useAuthStore().userInfo;
const conversationInfo = ref(null)
// --- 消息数据 ---
const messages = ref([]);
watch(
() => chatStore.messages,
async (newVal) => {
scrollToBottom();
conversationStore.conversations.find(x => x.id == conversationInfo.value.id).unreadCount = 0;
signalRStore.clearUnreadCount(conversationInfo.value.id);
},
{deep: true}
);
// 自动滚动到底部
const scrollToBottom = async () => {
await nextTick(); // 等待 DOM 更新后执行
if (historyRef.value) {
historyRef.value.scrollTop = historyRef.value.scrollHeight;
}
};
// 发送文本
async function sendText() {
if (!input.value.trim()) return;
// 根据 C# MessageBaseDto 构造的示例对象
const msg = {
type: "Text", // 消息类型,例如 'Text', 'Image', 'File'
chatType: "PRIVATE", // 'PRIVATE' 或 'GROUP'
senderId: conversationInfo.value.userId, // 当前用户ID (对应 int)
receiverId: conversationInfo.value.targetId, // 接收者ID (对应 int)
content: input.value,
timeStamp: new Date().toISOString() // 对应 DateTime建议存标准 ISO 字符串
};
msg.isLoading = false;
await signalRStore.sendMsg(msg);
input.value = ''; // 清空输入框
msg.isLoading = false;
}
// 通话模拟
function startCall(type) {
console.log(`发起${type === 'video' ? '视频' : '语音'}通话`);
}
// 文件处理模拟
function handleFile(e) {
const file = e.target.files[0];
if (file) console.log('选中文件:', file.name);
}
function toggleEmoji() {
console.log('打开表情面板');
}
async function loadConversation(conversationId) {
/*
const res = await messageService.getConversationById(conversationId);
conversationInfo.value = res.data;
*/
if(conversationStore.conversations.length == 0){
await conversationStore.loadUserConversations();
}
conversationInfo.value = conversationStore.conversations.find(x => x.id == Number(conversationId));
}
const initChat = async (newId) => {
await loadConversation(newId);
const sessionid = generateSessionId(conversationInfo.value.userId, conversationInfo.value.targetId)
await chatStore.swtichSession(sessionid,newId);
scrollToBottom();
}
// 监听路由参数
watch(
() => props.id,
async (newId) => {
await initChat(newId)
},
{ immediate: true } // 组件第一次挂载(刷新页面进入)时会立即执行一次
)
</script>
<style scoped>
/* 核心布局修复 */
.chat-panel {
display: flex;
flex-direction: column;
height: 100%; /* 确保占满父容器 */
background: #f5f5f5;
}
.chat-header {
height: 60px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.tool-btn {
border: 0;
background-color: white;
}
/* 历史区域:自动撑开并处理滚动 */
.chat-history {
flex: 1;
overflow-y: auto;
padding: 20px 30px;
}
.chat-footer {
height: 160px;
flex-shrink: 0;
background: #fff;
border-top: 1px solid #e0e0e0;
padding: 10px 20px;
display: flex;
flex-direction: column;
}
/* 消息对齐逻辑 */
.msg {
display: flex;
margin-bottom: 24px;
gap: 12px;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loaderIcon {
display: inline-block; /* 必须是 block 或 inline-block 才能旋转 */
line-height: 0;
animation: spin 1s linear infinite; /* 1秒转一圈线性速度无限循环 */
}
.status {
position: absolute;
top: 30%;
left: -20px;
}
.msg.mine {
flex-direction: row-reverse;
}
.msg.mine .msg-content {
align-items: flex-end;
}
.bubble {
padding: 9px 14px;
border-radius: 6px;
font-size: 14px;
max-width: 450px;
word-break: break-all;
background: #fff;
position: relative;
display: inline-block;
}
.mine .bubble {
background: #95ec69;
}
.avatar-chat {
width: 38px;
height: 38px;
border-radius: 4px;
flex-shrink: 0;
}
.msg-time {
font-size: 11px;
color: #b2b2b2;
margin-top: 4px;
display: block;
}
textarea {
flex: 1;
border: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 14px;
}
.send-row {
display: flex;
justify-content: flex-end;
}
.send-btn {
background: #f5f5f5;
color: #07c160;
border: 1px solid #e0e0e0;
padding: 5px 20px;
border-radius: 4px;
cursor: pointer;
}
</style>