IM/frontend/web/src/views/messages/MessageList.vue
nanxun a31735dbe2 前端:
修复了已知问题
后端:
修复了已知问题
2026-01-22 15:25:54 +08:00

319 lines
8.6 KiB
Vue

<template>
<div id="MsgList">
<aside class="list-panel">
<div class="search-section">
<div class="search-box">
<span class="search-icon"><i v-html="feather.icons['search'].toSvg({width:15,height:15})"></i></span>
<input v-model="searchQuery" placeholder="搜索" />
</div>
<div class="addMenu">
<AddMenu :menu-list="addMenuList" @action-active="actionHandler"/>
</div>
</div>
<div v-if="msgTitleShow" class="showMsg" @click="requestNotificationPermission">
<i style="color: red;line-height:0;" v-html="feather.icons['alert-circle'].toSvg({width:14})"></i>
<span>新消息无法通知,点我授予通知权限</span>
</div>
<div class="scroll-area">
<div v-for="s in filteredSessions" :key="s.id"
class="list-item" :class="{active: activeId === s.id}" @click="selectSession(s)">
<div class="avatar-container">
<img :src="s.targetAvatar ?? defaultAvatar" class="avatar-std" />
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
</div>
<div class="info">
<div class="name-row">
<span class="name">{{ s.targetName ?? '未知用户' }}</span>
<span class="time">{{ formatDate(s.dateTime) ?? '1970/1/1 00:00:00' }}</span>
</div>
<div class="last-msg">{{ s.lastMessage ?? '获取消息内容失败' }}</div>
</div>
</div>
</div>
</aside>
<RouterView></RouterView>
<SearchUser v-model="searchUserModal"/>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { ref, computed, nextTick, onMounted } from 'vue'
import { messageService } from '@/services/message'
import defaultAvatar from '@/assets/default_avatar.png'
import { formatDate } from '@/utils/formatDate'
import { useConversationStore } from '@/stores/conversation'
import AddMenu from '@/components/addMenu.vue'
import feather from 'feather-icons'
import SearchUser from '@/components/user/SearchUser.vue'
import { useBrowserNotification } from '@/services/useBrowserNotification'
const conversationStore = useConversationStore();
const router = useRouter();
const browserNotification = useBrowserNotification();
const searchQuery = ref('')
const activeId = ref(1)
const conversations = ref([]);
const searchUserModal = ref(false);
const msgTitleShow = ref(false);
const addMenuList = [
{
text: '发起群聊',
action: 'createGroup',
// 气泡/对话图标
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`
},
{
text: '添加朋友',
action: 'addFriend',
// 人像+号图标
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="17" y1="11" x2="23" y2="11"></line></svg>`
},
{
text: '新建笔记',
action: 'newNote',
// 书本/笔记本图标
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>`
}
];
const filteredSessions = computed(() => conversationStore.sortedConversations.filter(s => s.targetName.includes(searchQuery.value)))
function selectSession(s) {
activeId.value = s.id
router.push(`/messages/chat/${s.id}`)
}
function actionHandler(type){
switch(type){
case 'addFriend':
searchUserModal.value = true;
break;
default:
break;
}
}
async function requestNotificationPermission(){
await browserNotification.requestPermission();
if(Notification.permission === "granted") msgTitleShow.value = false;
}
async function loadConversation() {
const res = await messageService.getConversations();
conversations.value = res.data;
}
onMounted(async () => {
await conversationStore.loadUserConversations();
if(Notification.permission != "granted") msgTitleShow.value = true;
})
</script>
<style scoped>
#MsgList {
display: flex;
flex: 1;
}
/* 2. 列表区修复 */
.list-panel {
width: 250px;
flex-shrink: 0;
background: #eee;
border-right: 1px solid #d6d6d6;
display: flex;
flex-direction: column;
}
.showMsg {
/* width: 10px; */
height: 20px;
background: #e3f98d;
font-size: 12px;
display: flex;
/* text-align: center; */
flex-wrap: nowrap;
align-content: center;
justify-content: center;
align-items: center;
color: red;
cursor: pointer;
}
/* 修复:搜索框美化 */
.search-section {
padding: 20px 12px 10px 12px;
display: flex;
}
.search-box {
flex: 9;
display: flex;
align-items: center;
background: #dbdbdb;
padding: 4px 8px;
border-radius: 4px;
gap: 5px;
}
.search-icon { font-size: 12px; color: #666; }
.search-box input {
background: transparent;
border: none;
outline: none;
font-size: 12px;
width: 100%;
}
.addMenu {
flex: 1;
padding-left: 5px;
}
.scroll-area { flex: 1; overflow-y: auto; }
/* 3. 聊天主面板修复 */
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #f5f5f5;
min-width: 0;
}
.chat-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
}
.chat-history {
flex: 1;
overflow-y: auto;
padding: 20px 30px;
}
/* 修复:本人消息右侧对齐逻辑 */
.msg {
display: flex;
margin-bottom: 24px;
gap: 12px;
}
/* 别人发的:默认靠左 */
.msg.other { flex-direction: row; }
/* 本人发的:翻转排列方向,靠右显示 */
.msg.mine {
flex-direction: row-reverse;
}
.msg-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
/* 修复:本人消息文字和时间戳也需要右对齐 */
.msg.mine .msg-content {
align-items: flex-end;
}
.bubble {
padding: 9px 14px;
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
word-break: break-all;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.other .bubble { background: #fff; color: #333; }
.mine .bubble { background: #95ec69; color: #000; }
.msg-time {
font-size: 11px;
color: #b2b2b2;
margin-top: 4px;
}
/* 头像样式统一 */
.avatar-std { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
/* 未读气泡 */
.avatar-container { position: relative; }
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background: #ff4d4f;
color: #fff;
font-size: 10px;
padding: 0 4px;
min-width: 16px;
height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #fff;
}
/* 输入框区域修复 */
.chat-footer {
height: 160px;
background: #fff;
border-top: 1px solid #e0e0e0;
padding: 10px 20px;
display: flex;
flex-direction: column;
}
.toolbar { display: flex; gap: 12px; margin-bottom: 5px; font-size: 20px; color: #666; }
.toolbar button { background: none; border: none; cursor: pointer; opacity: 0.7; }
textarea {
flex: 1;
border: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 14px;
padding: 5px 0;
}
.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;
font-size: 13px;
}
.send-btn:hover { background: #e2e2e2; }
/* 列表美化 */
.list-item { display: flex; padding: 12px; gap: 12px; cursor: pointer; }
.list-item.active { background: #c6c6c6; }
.list-item:hover:not(.active) { background: #ddd; }
.info { flex: 1; overflow: hidden; }
.name-row { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 14px; font-weight: 500; }
.time { font-size: 11px; color: #999; }
.last-msg { font-size: 12px; color: #888; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item { font-size: 24px; cursor: pointer; opacity: 0.5; }
.nav-item.active { opacity: 1; }
</style>