feat:单聊页面bug修复+后台管理端好友页面bug修复+项目进度、记忆文件更新
This commit is contained in:
@@ -1,14 +1,44 @@
|
||||
<script>
|
||||
/**
|
||||
* 应用入口
|
||||
*
|
||||
* 全局生命周期:
|
||||
* - onLaunch: 初始化 WebSocket 连接(如果用户已登录)和事件监听
|
||||
* - onShow: 检查 WebSocket 连接状态并恢复
|
||||
*/
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useWebSocketStore } from '@/store/websocket'
|
||||
import { useChatStore } from '@/store/chat'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
onLaunch() {
|
||||
console.log('App Launch')
|
||||
this._initGlobalWS()
|
||||
},
|
||||
onShow: function () {
|
||||
onShow() {
|
||||
console.log('App Show')
|
||||
const userStore = useUserStore()
|
||||
const wsStore = useWebSocketStore()
|
||||
if (userStore.isLoggedIn && !wsStore.isConnected) {
|
||||
wsStore.connect()
|
||||
}
|
||||
},
|
||||
onHide: function () {
|
||||
onHide() {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
_initGlobalWS() {
|
||||
const userStore = useUserStore()
|
||||
if (!userStore.isLoggedIn) return
|
||||
const wsStore = useWebSocketStore()
|
||||
wsStore.connect()
|
||||
const chatStore = useChatStore()
|
||||
const contactStore = useContactStore()
|
||||
chatStore.initWsListeners()
|
||||
contactStore.initWsListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
* 登录成功后 reLaunch 到首页
|
||||
*/
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useWebSocketStore } from '@/store/websocket'
|
||||
import { useChatStore } from '@/store/chat'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
|
||||
export default {
|
||||
name: 'LoginPage',
|
||||
@@ -152,6 +155,10 @@ export default {
|
||||
account: this.form.account.trim(),
|
||||
password: this.form.password
|
||||
})
|
||||
const wsStore = useWebSocketStore()
|
||||
wsStore.connect()
|
||||
useChatStore().initWsListeners()
|
||||
useContactStore().initWsListeners()
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => uni.reLaunch({ url: '/pages/index/index' }), 800)
|
||||
} catch (e) {
|
||||
|
||||
@@ -222,8 +222,13 @@ export default {
|
||||
finally { loadingMore.value = false }
|
||||
}
|
||||
|
||||
const canRecall = (msg) => {
|
||||
if (!msg.created_at) return false
|
||||
return (Date.now() - new Date(msg.created_at).getTime()) < 2 * 60 * 1000
|
||||
}
|
||||
|
||||
const onMsgLongPress = (msg) => {
|
||||
if (!isSelf(msg) || msg.status === 2) return
|
||||
if (!isSelf(msg) || msg.status === 2 || !canRecall(msg)) return
|
||||
uni.showActionSheet({
|
||||
itemList: ['撤回'],
|
||||
success: (res) => {
|
||||
@@ -304,9 +309,12 @@ export default {
|
||||
/* ===== 消息列表 ===== */
|
||||
.msg-list {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
padding: 16rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.load-more { text-align: center; padding: 16rpx 0; }
|
||||
.load-more-text { font-size: 24rpx; color: #94A3B8; }
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
focus
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @tap="keyword = ''; groups = []; searched = false">
|
||||
<uni-icons type="clear" size="18" color="#94A3B8" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-cancel" @tap="goBack">
|
||||
<text class="search-cancel-text">取消</text>
|
||||
@@ -29,28 +32,35 @@
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<scroll-view scroll-y class="result-list">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="searched && results.length === 0" class="empty-state">
|
||||
<text class="empty-text">未找到相关消息</text>
|
||||
<view v-if="searching" class="status-state">
|
||||
<text class="status-text">搜索中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 结果列表 -->
|
||||
<view
|
||||
v-for="item in results"
|
||||
:key="item.id"
|
||||
class="result-item"
|
||||
@tap="openChat(item)"
|
||||
>
|
||||
<view class="result-avatar-wrap">
|
||||
<image v-if="item.sender_avatar" class="result-avatar" :src="item.sender_avatar" mode="aspectFill" />
|
||||
<view v-else class="result-avatar result-avatar-placeholder">
|
||||
<text class="avatar-text">{{ (item.sender_nickname || '?')[0] }}</text>
|
||||
<view v-else-if="searched && groups.length === 0" class="status-state">
|
||||
<text class="status-text">未找到相关消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 按会话分组展示 -->
|
||||
<view v-for="group in groups" :key="group.conversation_id" class="result-group">
|
||||
<view class="group-header" @tap="openConversation(group)">
|
||||
<view class="group-avatar-wrap">
|
||||
<image v-if="group.peer_avatar" class="group-avatar" :src="group.peer_avatar" mode="aspectFill" />
|
||||
<view v-else class="group-avatar group-avatar-placeholder">
|
||||
<text class="avatar-text">{{ (group.peer_name || '?')[0] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="group-name">{{ group.peer_name || '未知用户' }}</text>
|
||||
<text class="group-count">{{ group.messages.length }} 条相关</text>
|
||||
</view>
|
||||
<view class="result-info">
|
||||
<text class="result-name">{{ item.sender_nickname || '未知用户' }}</text>
|
||||
<text class="result-content">{{ item.content }}</text>
|
||||
<text class="result-time">{{ item.created_at }}</text>
|
||||
<view
|
||||
v-for="msg in group.messages"
|
||||
:key="msg.id"
|
||||
class="result-item"
|
||||
@tap="openConversation(group)"
|
||||
>
|
||||
<text class="result-sender">{{ msg.sender_nickname || '未知' }}:</text>
|
||||
<text class="result-content">{{ msg.content }}</text>
|
||||
<text class="result-time">{{ msg.created_at }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -58,32 +68,66 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import imApi from '@/api/im'
|
||||
import { useChatStore } from '@/store/chat'
|
||||
|
||||
export default {
|
||||
name: 'ChatSearch',
|
||||
setup() {
|
||||
const chatStore = useChatStore()
|
||||
const keyword = ref('')
|
||||
const results = ref([])
|
||||
const groups = ref([])
|
||||
const searched = ref(false)
|
||||
const searching = ref(false)
|
||||
|
||||
let debounceTimer = null
|
||||
watch(keyword, (val) => {
|
||||
clearTimeout(debounceTimer)
|
||||
if (!val.trim()) { groups.value = []; searched.value = false; return }
|
||||
debounceTimer = setTimeout(() => onSearch(), 300)
|
||||
})
|
||||
|
||||
const onSearch = async () => {
|
||||
const kw = keyword.value.trim()
|
||||
if (!kw) return
|
||||
|
||||
searching.value = true
|
||||
searched.value = true
|
||||
try {
|
||||
const res = await imApi.searchMessages(kw)
|
||||
results.value = (res.data && res.data.list) || []
|
||||
const list = (res.data && res.data.list) || []
|
||||
groups.value = _groupByConversation(list)
|
||||
} catch {
|
||||
results.value = []
|
||||
groups.value = []
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openChat = (item) => {
|
||||
/** 将扁平消息列表按 conversation_id 分组,附加会话的 peer 信息 */
|
||||
const _groupByConversation = (messages) => {
|
||||
const map = {}
|
||||
for (const msg of messages) {
|
||||
const convId = msg.conversation_id
|
||||
if (!map[convId]) {
|
||||
const conv = chatStore.conversationList.find(c => c.id === convId)
|
||||
map[convId] = {
|
||||
conversation_id: convId,
|
||||
peer_name: conv?.peer_nickname || msg.sender_nickname || '未知',
|
||||
peer_avatar: conv?.peer_avatar || msg.sender_avatar || '',
|
||||
peer_id: conv?.peer_user_id || 0,
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
map[convId].messages.push(msg)
|
||||
}
|
||||
return Object.values(map)
|
||||
}
|
||||
|
||||
const openConversation = (group) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/conversation?conversationId=${item.conversation_id}&peerId=${item.sender_id}&peerName=${encodeURIComponent(item.sender_nickname || '')}&peerAvatar=${encodeURIComponent(item.sender_avatar || '')}`
|
||||
url: `/pages/chat/conversation?conversationId=${group.conversation_id}&peerId=${group.peer_id}&peerName=${encodeURIComponent(group.peer_name)}&peerAvatar=${encodeURIComponent(group.peer_avatar)}`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,14 +135,7 @@ export default {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
return {
|
||||
keyword,
|
||||
results,
|
||||
searched,
|
||||
onSearch,
|
||||
openChat,
|
||||
goBack
|
||||
}
|
||||
return { keyword, groups, searched, searching, onSearch, openConversation, goBack }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -127,12 +164,8 @@ export default {
|
||||
height: 68rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1E293B;
|
||||
}
|
||||
.search-input { flex: 1; font-size: 28rpx; color: #1E293B; }
|
||||
.search-clear { min-width: 44rpx; min-height: 44rpx; display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
.search-cancel {
|
||||
margin-left: 20rpx;
|
||||
@@ -142,92 +175,42 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.search-cancel-text { font-size: 28rpx; color: #2563EB; }
|
||||
.search-cancel:active { opacity: 0.6; }
|
||||
|
||||
.search-cancel-text {
|
||||
font-size: 28rpx;
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.search-cancel:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
height: calc(100vh - 100rpx);
|
||||
.result-list { height: calc(100vh - 100rpx); }
|
||||
|
||||
/* 分组 */
|
||||
.result-group { margin-bottom: 16rpx; }
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 32rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 1rpx solid #E2E8F0;
|
||||
}
|
||||
.group-header:active { background-color: #F1F5F9; }
|
||||
.group-avatar-wrap { flex-shrink: 0; margin-right: 16rpx; }
|
||||
.group-avatar { width: 64rpx; height: 64rpx; border-radius: 16rpx; }
|
||||
.group-avatar-placeholder { background-color: #2563EB; display: flex; align-items: center; justify-content: center; }
|
||||
.avatar-text { color: #FFFFFF; font-size: 26rpx; font-weight: 600; }
|
||||
.group-name { flex: 1; font-size: 30rpx; font-weight: 500; color: #1E293B; }
|
||||
.group-count { font-size: 24rpx; color: #94A3B8; }
|
||||
|
||||
/* 消息条目 */
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
padding: 16rpx 32rpx 16rpx 112rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 1rpx solid #F1F5F9;
|
||||
transition: background-color 150ms ease;
|
||||
border-bottom: 1rpx solid #F8FAFC;
|
||||
}
|
||||
.result-item:active { background-color: #F1F5F9; }
|
||||
.result-sender { font-size: 26rpx; color: #64748B; flex-shrink: 0; }
|
||||
.result-content { flex: 1; font-size: 26rpx; color: #1E293B; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.result-time { font-size: 22rpx; color: #94A3B8; margin-left: 12rpx; flex-shrink: 0; }
|
||||
|
||||
.result-item:active {
|
||||
background-color: #F1F5F9;
|
||||
}
|
||||
|
||||
.result-avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.result-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.result-avatar-placeholder {
|
||||
background-color: #2563EB;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #1E293B;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 26rpx;
|
||||
color: #64748B;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
/* 状态 */
|
||||
.status-state { display: flex; align-items: center; justify-content: center; padding-top: 200rpx; }
|
||||
.status-text { font-size: 28rpx; color: #94A3B8; }
|
||||
</style>
|
||||
|
||||
@@ -67,9 +67,13 @@ export default {
|
||||
|
||||
const togglePin = async () => {
|
||||
const newVal = !isPinned.value
|
||||
await chatStore.pinConversation(conversationId.value, newVal)
|
||||
isPinned.value = newVal
|
||||
uni.showToast({ title: newVal ? '已置顶' : '已取消置顶', icon: 'none' })
|
||||
try {
|
||||
await chatStore.pinConversation(conversationId.value, newVal)
|
||||
isPinned.value = newVal
|
||||
uni.showToast({ title: newVal ? '已置顶' : '已取消置顶', icon: 'none' })
|
||||
} catch {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const onClearHistory = () => {
|
||||
@@ -78,8 +82,12 @@ export default {
|
||||
content: '确定清空聊天记录?此操作不可恢复',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await chatStore.clearHistory(conversationId.value)
|
||||
uni.showToast({ title: '已清空', icon: 'none' })
|
||||
try {
|
||||
await chatStore.clearHistory(conversationId.value)
|
||||
uni.showToast({ title: '已清空', icon: 'none' })
|
||||
} catch {
|
||||
uni.showToast({ title: '清空失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -91,8 +99,12 @@ export default {
|
||||
content: '确定删除该会话?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await chatStore.deleteConversation(conversationId.value)
|
||||
uni.navigateBack({ delta: 2 })
|
||||
try {
|
||||
await chatStore.deleteConversation(conversationId.value)
|
||||
uni.navigateBack({ delta: 2 })
|
||||
} catch {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -97,6 +97,7 @@ const handleAccept = async (requestId) => {
|
||||
uni.showToast({ title: '已接受', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('接受好友申请失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '接受申请失败', icon: 'none' })
|
||||
} finally {
|
||||
processingMap[requestId] = false
|
||||
}
|
||||
@@ -110,6 +111,7 @@ const handleReject = async (requestId) => {
|
||||
uni.showToast({ title: '已拒绝', icon: 'none' })
|
||||
} catch (e) {
|
||||
console.error('拒绝好友申请失败', e)
|
||||
uni.showToast({ title: e?.data?.message || '拒绝申请失败', icon: 'none' })
|
||||
} finally {
|
||||
processingMap[requestId] = false
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<view v-else-if="searchResults.length > 0" class="result-list">
|
||||
<view
|
||||
v-for="user in searchResults"
|
||||
:key="user.user_id"
|
||||
:key="user.id"
|
||||
class="user-item"
|
||||
>
|
||||
<view class="avatar" :style="{ backgroundColor: getAvatarColor(user.nickname || user.username) }">
|
||||
@@ -53,10 +53,10 @@
|
||||
<view
|
||||
v-else
|
||||
class="add-btn"
|
||||
:class="{ 'add-btn--disabled': addingMap[user.user_id] }"
|
||||
:class="{ 'add-btn--disabled': addingMap[user.id] }"
|
||||
@tap="showAddDialog(user)"
|
||||
>
|
||||
<text class="add-btn-text">{{ addingMap[user.user_id] ? '已发送' : '添加' }}</text>
|
||||
<text class="add-btn-text">{{ addingMap[user.id] ? '已发送' : '添加' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -79,7 +79,7 @@
|
||||
<view v-else-if="recommendations.length > 0" class="result-list">
|
||||
<view
|
||||
v-for="user in recommendations"
|
||||
:key="user.user_id"
|
||||
:key="user.id"
|
||||
class="user-item"
|
||||
>
|
||||
<view class="avatar" :style="{ backgroundColor: getAvatarColor(user.nickname || user.username) }">
|
||||
@@ -91,10 +91,10 @@
|
||||
</view>
|
||||
<view
|
||||
class="add-btn"
|
||||
:class="{ 'add-btn--disabled': addingMap[user.user_id] }"
|
||||
:class="{ 'add-btn--disabled': addingMap[user.id] }"
|
||||
@tap="showAddDialog(user)"
|
||||
>
|
||||
<text class="add-btn-text">{{ addingMap[user.user_id] ? '已发送' : '添加' }}</text>
|
||||
<text class="add-btn-text">{{ addingMap[user.id] ? '已发送' : '添加' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -141,7 +141,7 @@ const doSearch = async () => {
|
||||
}
|
||||
|
||||
const showAddDialog = (user) => {
|
||||
if (addingMap[user.user_id]) return
|
||||
if (addingMap[user.id]) return
|
||||
uni.showModal({
|
||||
title: '添加好友',
|
||||
editable: true,
|
||||
@@ -149,13 +149,17 @@ const showAddDialog = (user) => {
|
||||
content: '',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
addingMap[user.user_id] = true
|
||||
addingMap[user.id] = true
|
||||
try {
|
||||
await contactStore.sendRequest(user.user_id, res.content || '')
|
||||
await contactStore.sendRequest(user.id, res.content || '')
|
||||
uni.showToast({ title: '申请已发送', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('发送好友申请失败', e)
|
||||
addingMap[user.user_id] = false
|
||||
addingMap[user.id] = false
|
||||
uni.showToast({
|
||||
title: e?.data?.message || '发送申请失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
// ==================== Computed ====================
|
||||
|
||||
/** 当前会话的消息列表 */
|
||||
/** 当前会话的消息列表(conversationId 为 null 时取 pending 队列) */
|
||||
const currentMessages = computed(() => {
|
||||
if (!currentConversationId.value) return []
|
||||
if (!currentConversationId.value) {
|
||||
return messagesMap.value['pending'] || []
|
||||
}
|
||||
return messagesMap.value[currentConversationId.value] || []
|
||||
})
|
||||
|
||||
@@ -98,9 +100,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
_sending: true
|
||||
}
|
||||
|
||||
if (conversationId) {
|
||||
_appendMessage(conversationId, tempMsg)
|
||||
}
|
||||
const displayConvId = conversationId || 'pending'
|
||||
_appendMessage(displayConvId, tempMsg)
|
||||
|
||||
pendingMessages.value[clientMsgId] = { status: 'sending', tempMsg }
|
||||
|
||||
@@ -226,12 +227,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
created_at: data.created_at
|
||||
})
|
||||
|
||||
_updateConversationPreview(data.conversation_id, data.content, data.created_at, data.sender_id)
|
||||
const existingConv = conversationList.value.find(c => c.id === data.conversation_id)
|
||||
if (existingConv) {
|
||||
_updateConversationPreview(data.conversation_id, data.content, data.created_at, data.sender_id)
|
||||
} else {
|
||||
fetchConversations()
|
||||
}
|
||||
|
||||
if (data.conversation_id !== currentConversationId.value) {
|
||||
const conv = conversationList.value.find(c => c.id === data.conversation_id)
|
||||
if (conv) {
|
||||
conv.unread_count = (conv.unread_count || 0) + 1
|
||||
if (existingConv) {
|
||||
existingConv.unread_count = (existingConv.unread_count || 0) + 1
|
||||
}
|
||||
totalUnread.value++
|
||||
} else {
|
||||
@@ -268,6 +273,12 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
if (pending.tempMsg && pending.tempMsg.conversation_id === 0 && convId) {
|
||||
currentConversationId.value = convId
|
||||
const pendingMsgs = messagesMap.value['pending'] || []
|
||||
if (pendingMsgs.length > 0) {
|
||||
if (!messagesMap.value[convId]) messagesMap.value[convId] = []
|
||||
messagesMap.value[convId].push(...pendingMsgs)
|
||||
delete messagesMap.value['pending']
|
||||
}
|
||||
}
|
||||
|
||||
const messages = messagesMap.value[convId]
|
||||
@@ -312,12 +323,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
// ==================== 内部工具方法 ====================
|
||||
|
||||
/** 追加消息到缓存 */
|
||||
/** 追加消息到缓存(带去重检查,防止重连/ACK 导致重复) */
|
||||
const _appendMessage = (conversationId, message) => {
|
||||
if (!messagesMap.value[conversationId]) {
|
||||
messagesMap.value[conversationId] = []
|
||||
}
|
||||
messagesMap.value[conversationId].push(message)
|
||||
const list = messagesMap.value[conversationId]
|
||||
const isDup = list.some(m =>
|
||||
(message.id && m.id === message.id) ||
|
||||
(message.client_msg_id && m.client_msg_id === message.client_msg_id)
|
||||
)
|
||||
if (!isDup) {
|
||||
list.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新会话列表中的预览信息 */
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import authApi from '@/api/auth'
|
||||
import { useWebSocketStore } from '@/store/websocket'
|
||||
import {
|
||||
saveToken,
|
||||
getToken,
|
||||
@@ -112,10 +113,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
* 3. 重置 Store 状态
|
||||
*/
|
||||
const logout = async () => {
|
||||
const wsStore = useWebSocketStore()
|
||||
wsStore.disconnect()
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch (e) {
|
||||
// 即使后端请求失败,也要清除本地状态(避免用户被卡住)
|
||||
console.warn('登出 API 调用失败,仍然清除本地状态', e)
|
||||
}
|
||||
token.value = ''
|
||||
|
||||
Reference in New Issue
Block a user