fix:修复已知问题
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="im-hover-card"
|
||||
:style="cardStyle"
|
||||
@mouseenter="clearTimer"
|
||||
@mouseleave="hide"
|
||||
>
|
||||
<div class="card-inner">
|
||||
<div class="user-profile">
|
||||
<div class="info-text">
|
||||
<h4 class="nickname">{{ currentUser.name }}</h4>
|
||||
<p class="detail-item">
|
||||
<span class="label">微信号:</span>
|
||||
<span class="value">{{ currentUser.id }}</span>
|
||||
</p>
|
||||
<p class="detail-item">
|
||||
<span class="label">地 区:</span>
|
||||
<span class="value">{{ currentUser.region || '未知' }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<img :src="currentUser.avatar" class="avatar-square" />
|
||||
</div>
|
||||
|
||||
<div class="user-bio">
|
||||
<p class="bio-text">{{ currentUser.bio || '暂无签名' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="action-btn primary" @click="onChat">发消息</button>
|
||||
<button v-if="!currentUser.isFriend" class="action-btn secondary" @click="onAdd">添加好友</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
|
||||
const isVisible = ref(false);
|
||||
const currentUser = ref({});
|
||||
const cardStyle = reactive({
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
left: '0px'
|
||||
});
|
||||
|
||||
let timer = null;
|
||||
|
||||
const show = (el, data) => {
|
||||
clearTimer();
|
||||
currentUser.value = data;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
// IM 习惯:通常在头像右侧或下方弹出
|
||||
// 这里设置为在头像中心水平对齐,下方弹出
|
||||
cardStyle.top = `${rect.bottom + 8}px`;
|
||||
cardStyle.left = `${rect.left}px`;
|
||||
|
||||
isVisible.value = true;
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
timer = setTimeout(() => {
|
||||
isVisible.value = false;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
console.log('申请添加好友:', currentUser.value.id);
|
||||
// 这里写你的逻辑
|
||||
};
|
||||
|
||||
const onChat = () => {
|
||||
console.log('跳转聊天窗口:', currentUser.value.id);
|
||||
isVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({ show, hide });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-hover-card {
|
||||
z-index: 9999;
|
||||
width: 280px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px; /* IM 通常是小圆角或直角 */
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #ebeef5;
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部布局:文字在左,头像在右(典型微信名片风) */
|
||||
.user-profile {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
color: #999;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.avatar-square {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 签名区 */
|
||||
.user-bio {
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #f2f2f2;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bio-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 底部按钮:去掉花哨渐变,改用纯色或文字链接感 */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #f2f2f2;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
color: #576b95; /* 经典的微信蓝/链接色 */
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
color: #3e4d6d;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
color: #576b95;
|
||||
}
|
||||
|
||||
/* 动画:简单的淡入 */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -10,10 +10,12 @@
|
||||
|
||||
<div class="chat-history" ref="historyRef">
|
||||
<HistoryLoading ref="loadingRef" :loading="isLoading" :finished="isFinished" :error="hasError" @retry="loadHistoryMsg"/>
|
||||
<UserHoverCard ref="userHoverCardRef"/>
|
||||
<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) : getAvatar(m.senderId) ?? defaultAvatar" class="avatar-chat" />
|
||||
<img @mouseenter="(e) => handleHoverCard(e,m)" @mouseleave="closeHoverCard" :src="m.senderId == myInfo.id ? (myInfo?.avatar || defaultAvatar) : m.senderAvatar ?? defaultAvatar" class="avatar-chat" />
|
||||
|
||||
<div class="msg-content">
|
||||
<div class="group-sendername" v-if="m.chatType == MESSAGE_TYPE.GROUP && m.senderId != myInfo.id">{{ m.senderName }}</div>
|
||||
<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>
|
||||
@@ -66,6 +68,7 @@ import { GetLocalIso } from '@/utils/dateTool';
|
||||
import HistoryLoading from '@/components/messages/HistoryLoading.vue';
|
||||
import { useMessage } from '@/components/messages/useAlert';
|
||||
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||
import UserHoverCard from '@/components/user/UserHoverCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id:{
|
||||
@@ -81,6 +84,7 @@ const conversationStore = useConversationStore();
|
||||
const input = ref(''); // 输入框内容
|
||||
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
||||
const loadingRef = ref(null)
|
||||
const userHoverCardRef = ref(null);
|
||||
const myInfo = useAuthStore().userInfo;
|
||||
|
||||
const conversationInfo = ref(null)
|
||||
@@ -122,6 +126,19 @@ const loadHistoryMsg = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleHoverCard = (e, m) => {
|
||||
const userInfo = {
|
||||
name: m.senderName,
|
||||
avatar: m.senderAvatar,
|
||||
id: m.senderId
|
||||
}
|
||||
userHoverCardRef.value.show(e.target, userInfo);
|
||||
}
|
||||
|
||||
const closeHoverCard = () => {
|
||||
userHoverCardRef.value.hide();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => chatStore.messages,
|
||||
async (newVal) => {
|
||||
@@ -132,10 +149,6 @@ watch(
|
||||
{deep: true}
|
||||
);
|
||||
|
||||
const getAvatar = (userId) => {
|
||||
return conversationStore.conversations.find(x => x.targetId == userId).targetAvatar;
|
||||
}
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick(); // 等待 DOM 更新后执行
|
||||
@@ -216,6 +229,7 @@ const initChat = async (newId) => {
|
||||
const sessionid = generateSessionId(
|
||||
conversationInfo.value.userId, conversationInfo.value.targetId, conversationInfo.value.chatType == MESSAGE_TYPE.GROUP)
|
||||
await chatStore.swtichSession(sessionid,newId);
|
||||
isFinished.value = false;
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -372,6 +386,12 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.group-sendername {
|
||||
width: 55px;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 消息对齐逻辑 */
|
||||
.msg {
|
||||
display: flex;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="scroll-area">
|
||||
<div v-for="s in filteredSessions" :key="s.id"
|
||||
class="list-item" :class="{active: activeId === s.id}" @click="selectSession(s)">
|
||||
class="list-item" :class="{active: activeId == s.id}" @click="selectSession(s)">
|
||||
<div class="avatar-container">
|
||||
<img :src="s.targetAvatar ? s.targetAvatar : defaultAvatar" class="avatar-std" />
|
||||
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import defaultAvatar from '@/assets/default_avatar.png'
|
||||
import { formatDate } from '@/utils/formatDate'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
@@ -50,13 +50,14 @@ import feather from 'feather-icons'
|
||||
import SearchUser from '@/components/user/SearchUser.vue'
|
||||
import CreateGroup from '@/components/groups/CreateGroup.vue'
|
||||
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
|
||||
const conversationStore = useConversationStore();
|
||||
const router = useRouter();
|
||||
const browserNotification = useBrowserNotification();
|
||||
|
||||
const searchQuery = ref('')
|
||||
const activeId = ref(1)
|
||||
const activeId = ref(0)
|
||||
const searchUserModal = ref(false);
|
||||
const createGroupModal = ref(false);
|
||||
const msgTitleShow = ref(false);
|
||||
@@ -100,6 +101,18 @@ function actionHandler(type){
|
||||
}
|
||||
}
|
||||
|
||||
const chatStore = useChatStore();
|
||||
|
||||
watch(
|
||||
() => chatStore.activeConversationId,
|
||||
(newVal) => {
|
||||
if(newVal && newVal != 0){
|
||||
activeId.value = newVal;
|
||||
}
|
||||
},
|
||||
{immediate:true}
|
||||
)
|
||||
|
||||
async function requestNotificationPermission(){
|
||||
await browserNotification.requestPermission();
|
||||
if(Notification.permission === "granted") msgTitleShow.value = false;
|
||||
|
||||
Reference in New Issue
Block a user