main #54
@ -1,5 +1,6 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.Models;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
@ -32,7 +33,7 @@ namespace IM_API.Hubs
|
|||||||
}
|
}
|
||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
public async Task SendPrivateMessage(MessageBaseDto dto)
|
public async Task SendMessage(MessageBaseDto dto)
|
||||||
{
|
{
|
||||||
if (!Context.User.Identity.IsAuthenticated)
|
if (!Context.User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
@ -41,8 +42,17 @@ namespace IM_API.Hubs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr),dto.ReceiverId,dto);
|
MessageBaseDto msgInfo = null;
|
||||||
await Clients.Users(dto.ReceiverId.ToString()).SendAsync("ReceiveMessage", dto);
|
if(dto.ChatType.ToLower() == ChatType.PRIVATE.ToString().ToLower())
|
||||||
|
{
|
||||||
|
msgInfo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clients.Users(dto.ReceiverId.ToString()).SendAsync("ReceiveMessage", msgInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ namespace IM_API.Interface.Services
|
|||||||
/// <param name="receiverId">接收人</param>
|
/// <param name="receiverId">接收人</param>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<bool> SendPrivateMessageAsync(int senderId,int receiverId,MessageBaseDto dto);
|
Task<MessageBaseDto> SendPrivateMessageAsync(int senderId,int receiverId,MessageBaseDto dto);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发送群聊消息
|
/// 发送群聊消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -19,7 +19,7 @@ namespace IM_API.Interface.Services
|
|||||||
/// <param name="groupId">接收群id</param>
|
/// <param name="groupId">接收群id</param>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<bool> SendGroupMessageAsync(int senderId,int groupId,MessageBaseDto dto);
|
Task<MessageBaseDto> SendGroupMessageAsync(int senderId,int groupId,MessageBaseDto dto);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取消息列表
|
/// 获取消息列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ namespace IM_API.Services
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
#region 发送群消息
|
#region 发送群消息
|
||||||
public async Task<bool> SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto)
|
public async Task<MessageBaseDto> SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto)
|
||||||
{
|
{
|
||||||
//判断群存在
|
//判断群存在
|
||||||
var isExist = await _context.Groups.AnyAsync(x => x.Id == groupId);
|
var isExist = await _context.Groups.AnyAsync(x => x.Id == groupId);
|
||||||
@ -92,12 +92,12 @@ namespace IM_API.Services
|
|||||||
message.StreamKey = StreamKeyBuilder.Group(groupId);
|
message.StreamKey = StreamKeyBuilder.Group(groupId);
|
||||||
_context.Messages.Add(message);
|
_context.Messages.Add(message);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return true;
|
return _mapper.Map<MessageBaseDto>(message);
|
||||||
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
#region 发送私聊消息
|
#region 发送私聊消息
|
||||||
public async Task<bool> SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto)
|
public async Task<MessageBaseDto> SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto)
|
||||||
{
|
{
|
||||||
bool isExist = await _context.Friends.AnyAsync(x => x.FriendId == receiverId);
|
bool isExist = await _context.Friends.AnyAsync(x => x.FriendId == receiverId);
|
||||||
if (!isExist) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
|
if (!isExist) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
|
||||||
@ -107,7 +107,7 @@ namespace IM_API.Services
|
|||||||
message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId);
|
message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId);
|
||||||
_context.Messages.Add(message);
|
_context.Messages.Add(message);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return true;
|
return _mapper.Map<MessageBaseDto>(message);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,19 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Alert from '@/components/messages/Alert.vue';
|
import Alert from '@/components/messages/Alert.vue';
|
||||||
</script>
|
import { onMounted } from 'vue';
|
||||||
|
import { useAuthStore } from './stores/auth';
|
||||||
|
//import { useSignalRStore } from './stores/signalr';
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const { useSignalRStore } = await import('./stores/signalr');
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const signalRStore = useSignalRStore();
|
||||||
|
if(authStore.token){
|
||||||
|
signalRStore.initSignalR();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|||||||
@ -64,7 +64,6 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
const refreshToken = authStore.refreshToken;
|
const refreshToken = authStore.refreshToken;
|
||||||
console.log(authStore)
|
|
||||||
if (refreshToken != null && refreshToken != '') {
|
if (refreshToken != null && refreshToken != '') {
|
||||||
const res = await authService.refresh(refreshToken)
|
const res = await authService.refresh(refreshToken)
|
||||||
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo)
|
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo)
|
||||||
|
|||||||
@ -8,6 +8,31 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
//判断是否已登录
|
//判断是否已登录
|
||||||
const isLoggedIn = computed(() => !!refreshToken.value);
|
const isLoggedIn = computed(() => !!refreshToken.value);
|
||||||
|
/**
|
||||||
|
* 安全解析 JWT
|
||||||
|
*/
|
||||||
|
const getPayload = (t) => {
|
||||||
|
try {
|
||||||
|
const base64Url = t.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
// 处理 Unicode 字符解码
|
||||||
|
return JSON.parse(decodeURIComponent(atob(base64).split('').map(c =>
|
||||||
|
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
||||||
|
).join('')));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查 Token 是否过期
|
||||||
|
const isTokenExpired = computed(() => {
|
||||||
|
if (!token.value) return true;
|
||||||
|
const payload = getPayload(token.value);
|
||||||
|
if (!payload || !payload.exp) return true;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
return (payload.exp - now) < 30; // 预留 30 秒缓冲
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录成功保存状态
|
* 登录成功保存状态
|
||||||
@ -36,5 +61,5 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
localStorage.removeItem('user_info')
|
localStorage.removeItem('user_info')
|
||||||
}
|
}
|
||||||
|
|
||||||
return { token, refreshToken, userInfo, isLoggedIn, setLoginInfo, logout };
|
return { token, refreshToken, userInfo, isLoggedIn, isTokenExpired, setLoginInfo, logout };
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { messagesDb } from "@/utils/db/messageDB";
|
||||||
|
import { messageService } from "@/services/message";
|
||||||
|
|
||||||
|
export const useChatStore = defineStore('chat', {
|
||||||
|
state: () => ({
|
||||||
|
activeSessionId: null,
|
||||||
|
activeConversationId: null,
|
||||||
|
messages: [],
|
||||||
|
pageSize: 20
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async addMessage(msg, sessionId) {
|
||||||
|
await messagesDb.save({ ...msg, sessionId });
|
||||||
|
this.messages.push({ ...msg, sessionId })
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 切换会话加载当前会话消息列表
|
||||||
|
* @param {*} sessionId
|
||||||
|
*/
|
||||||
|
async swtichSession(sessionId, conversationId) {
|
||||||
|
this.activeSessionId = sessionId;
|
||||||
|
this.activeConversationId = conversationId;
|
||||||
|
this.messages = [];
|
||||||
|
//先从浏览器缓存加载一部分消息列表
|
||||||
|
const localHistory = await messagesDb.getPageMessages(sessionId, new Date().toISOString(), this.pageSize);
|
||||||
|
console.log(localHistory)
|
||||||
|
if (localHistory.length > 0) {
|
||||||
|
this.messages = localHistory;
|
||||||
|
} else {
|
||||||
|
//如果本地没有消息数据则从后端拉取数据
|
||||||
|
const conversation = (await messageService.getConversationById(this.activeConversationId)).data;
|
||||||
|
const serverHistoryMsg = await this.fetchHistoryFromServer(this.activeConversationId, conversation.lastReadMessageId);
|
||||||
|
//对消息进行过滤,防止重复消息
|
||||||
|
const filterMsg = serverHistoryMsg.filter(m => !this.messages.find(exist => exist.msgId === m.msgId));
|
||||||
|
this.messages = [...filterMsg, ...this.messages]
|
||||||
|
}
|
||||||
|
//拉取新消息
|
||||||
|
this.fetchNewMsgFromServier(this.activeConversationId).then((newMsg) => {
|
||||||
|
//去重
|
||||||
|
const filterNewMsg = newMsg.filter(m => !this.messages.find(exist => exist.msgId === m.msgId));
|
||||||
|
this.messages = [...filterNewMsg, ...this.messages]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 从服务器加载新消息
|
||||||
|
* @param {*} sessionId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async fetchNewMsgFromServier(conversationId) {
|
||||||
|
const newMsg = (await messageService.getMessages(conversationId)).data;
|
||||||
|
if (newMsg.length > 0) {
|
||||||
|
const sessionId = this.activeSessionId;
|
||||||
|
await Promise.all(newMsg.map(msg =>
|
||||||
|
messagesDb.save({ ...msg, sessionId })
|
||||||
|
));
|
||||||
|
return newMsg;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 从服务器加载历史消息
|
||||||
|
* @param {*} sessionId
|
||||||
|
* @param {*} msgId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async fetchHistoryFromServer(conversationId, msgId) {
|
||||||
|
const res = (await messageService.getHistoryMessages(conversationId, msgId, this.pageSize)).data;
|
||||||
|
|
||||||
|
if (res.length > 0) {
|
||||||
|
const sessionId = this.activeSessionId;
|
||||||
|
await Promise.all(res.map(msg =>
|
||||||
|
messagesDb.save({ ...msg, sessionId })
|
||||||
|
));
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 加载更多历史消息
|
||||||
|
*/
|
||||||
|
async loadMoreMessages() {
|
||||||
|
const lastTimeStamp = this.messages.length > 0 ? this.messages[0].timeStamp : new Date().toISOString();
|
||||||
|
const history = await messagesDb.getPageMessages(this.activeSessionId, lastTimeStamp, this.pageSize);
|
||||||
|
if (history.length > 0) {
|
||||||
|
this.messages = [...history, ...this.messages]
|
||||||
|
} else {
|
||||||
|
const fetchMsg = await this.fetchHistoryFromServer(this.conversationId, this.messages[0].msgId);
|
||||||
|
const newMsgs = fetchMsg.filter(m => !this.messages.find(exist => exist.msgId === m.msgId));
|
||||||
|
this.messages = [...newMsgs, ...this.messages]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -2,9 +2,8 @@ import { defineStore } from "pinia";
|
|||||||
import * as signalR from '@microsoft/signalr';
|
import * as signalR from '@microsoft/signalr';
|
||||||
import { useMessage } from "@/components/messages/useAlert";
|
import { useMessage } from "@/components/messages/useAlert";
|
||||||
import { useAuthStore } from "./auth";
|
import { useAuthStore } from "./auth";
|
||||||
|
import { useChatStore } from "./chat";
|
||||||
const message = useMessage()
|
import { authService } from "@/services/auth";
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
export const useSignalRStore = defineStore('signalr', {
|
export const useSignalRStore = defineStore('signalr', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@ -13,10 +12,16 @@ export const useSignalRStore = defineStore('signalr', {
|
|||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async initSignalR() {
|
async initSignalR() {
|
||||||
|
const message = useMessage()
|
||||||
|
const authStore = useAuthStore()
|
||||||
const url = import.meta.env.VITE_SIGNALR_BASE_URL || 'http://localhost:5202/chat';
|
const url = import.meta.env.VITE_SIGNALR_BASE_URL || 'http://localhost:5202/chat';
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl(url, {
|
.withUrl(url, {
|
||||||
accessTokenFactory: () => {
|
accessTokenFactory: async () => {
|
||||||
|
if (authStore.isTokenExpired) {
|
||||||
|
const res = await authService.refresh(authStore.refreshToken)
|
||||||
|
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo)
|
||||||
|
}
|
||||||
return authStore.token;
|
return authStore.token;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -32,13 +37,35 @@ export const useSignalRStore = defineStore('signalr', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
registerHandlers() {
|
registerHandlers() {
|
||||||
|
const chatStore = useChatStore()
|
||||||
this.connection.on('ReceiveMessage', (msg) => {
|
this.connection.on('ReceiveMessage', (msg) => {
|
||||||
console.log(msg)
|
chatStore.addMessage(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.connection.onclose(() => { this.isConnected = false });
|
this.connection.onclose(() => { this.isConnected = false });
|
||||||
this.connection.onreconnected(() => { this.isConnected = true });
|
this.connection.onreconnected(() => { this.isConnected = true });
|
||||||
|
|
||||||
|
},
|
||||||
|
async sendMsg(msg) {
|
||||||
|
const message = useMessage()
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
if (!this.isConnected) {
|
||||||
|
message.error('与服务器连接中断,请重连后尝试...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 后端 Hub 定义的方法名通常为 SendMessage
|
||||||
|
// 参数顺序需要与后端 ChatHub 中的方法签名一致
|
||||||
|
if (msg.msgId == null) {
|
||||||
|
msg.msgId = self.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
await this.connection.invoke("SendMessage", msg);
|
||||||
|
chatStore.addMessage(msg);
|
||||||
|
console.log("消息发送成功!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("消息发送失败:", err);
|
||||||
|
message.error("消息发送失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -22,5 +22,21 @@ export const messagesDb = {
|
|||||||
},
|
},
|
||||||
async clearAll() {
|
async clearAll() {
|
||||||
return (await dbPromise).clear(STORE_NAME);
|
return (await dbPromise).clear(STORE_NAME);
|
||||||
|
},
|
||||||
|
async getPageMessages(sessionId, beforeTimeStamp, limit = 20) {
|
||||||
|
const db = await dbPromise;
|
||||||
|
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||||
|
const index = tx.store.index('by-sessionId');
|
||||||
|
const range = IDBKeyRange.only(sessionId);
|
||||||
|
let cursor = await index.openCursor(range, 'prev');
|
||||||
|
const results = [];
|
||||||
|
// 如果之前有消息,跳过已经加载的部分(这里简单演示,实际建议用 offset 或 ID)
|
||||||
|
while (cursor && results.length < limit) {
|
||||||
|
if (cursor.value.timeStamp < beforeTimeStamp) {
|
||||||
|
results.unshift(cursor.value); // 插到数组开头,保持时间正序
|
||||||
|
}
|
||||||
|
cursor = await cursor.continue();
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
11
frontend/web/src/utils/sessionIdTools.js
Normal file
11
frontend/web/src/utils/sessionIdTools.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 生成唯一的会话 ID (私聊)
|
||||||
|
* @param {string|number} id1 用户A的ID
|
||||||
|
* @param {string|number} id2 用户B的ID
|
||||||
|
*/
|
||||||
|
export const generateSessionId = (id1, id2) => {
|
||||||
|
// 1. 转换为字符串并放入数组
|
||||||
|
// 2. 排序(确保顺序一致性)
|
||||||
|
// 3. 用下划线或其他分隔符拼接
|
||||||
|
return [String(id1), String(id2)].sort().join('_');
|
||||||
|
};
|
||||||
@ -98,7 +98,7 @@ const handleLogin = async () => {
|
|||||||
if(res.code === 0){ // Assuming 0 is success
|
if(res.code === 0){ // Assuming 0 is success
|
||||||
message.success('登录成功')
|
message.success('登录成功')
|
||||||
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
|
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
|
||||||
signalRStore.initSignalR(res.data.token);
|
signalRStore.initSignalR();
|
||||||
router.push('/messages')
|
router.push('/messages')
|
||||||
}else{
|
}else{
|
||||||
message.error(res.message || '登录失败')
|
message.error(res.message || '登录失败')
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="chat-history" ref="historyRef">
|
<div class="chat-history" ref="historyRef">
|
||||||
<div v-for="m in messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
|
<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" />
|
<img :src="m.senderId == myInfo.id ? (myInfo?.avatar || defaultAvatar) : defaultAvatar" class="avatar-chat" />
|
||||||
|
|
||||||
<div class="msg-content">
|
<div class="msg-content">
|
||||||
@ -42,11 +42,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, nextTick, onMounted } from 'vue';
|
import { ref, computed, nextTick, onMounted, watch } from 'vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import defaultAvatar from '@/assets/default_avatar.png';
|
import defaultAvatar from '@/assets/default_avatar.png';
|
||||||
import { messageService } from '@/services/message';
|
import { messageService } from '@/services/message';
|
||||||
import { formatDate } from '@/utils/formatDate';
|
import { formatDate } from '@/utils/formatDate';
|
||||||
|
import { useChatStore } from '@/stores/chat';
|
||||||
|
import { generateSessionId } from '@/utils/sessionIdTools';
|
||||||
|
import { useSignalRStore } from '@/stores/signalr';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id:{
|
id:{
|
||||||
@ -54,26 +57,26 @@ const props = defineProps({
|
|||||||
required:true
|
required:true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const signalRStore = useSignalRStore();
|
||||||
|
|
||||||
const input = ref(''); // 输入框内容
|
const input = ref(''); // 输入框内容
|
||||||
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
||||||
const myInfo = useAuthStore().userInfo;
|
const myInfo = useAuthStore().userInfo;
|
||||||
|
|
||||||
const conversationInfo = ref(null)
|
const conversationInfo = ref(null)
|
||||||
|
|
||||||
// --- 模拟会话数据 (实际开发中应从后端或 store 获取) ---
|
|
||||||
const sessions = ref([
|
|
||||||
{ id: 1, name: '南浔', avatar: 'https://i.pravatar.cc/40?1', last: '' },
|
|
||||||
{ id: 2, name: '技术群', avatar: 'https://i.pravatar.cc/40?2', last: '' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// --- 消息数据 ---
|
// --- 消息数据 ---
|
||||||
const messages = ref({
|
const messages = ref([]);
|
||||||
1: [
|
|
||||||
{ id: 1, type: 'text', content: '在干嘛?', mine: false, time: '14:00' },
|
watch(
|
||||||
{ id: 2, type: 'text', content: '在写代码呢,帮你调样式', mine: true, time: '14:02' }
|
() => chatStore.messages,
|
||||||
],
|
async (newVal) => {
|
||||||
2: []
|
scrollToBottom();
|
||||||
});
|
},
|
||||||
|
{deep: true}
|
||||||
|
);
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
const scrollToBottom = async () => {
|
const scrollToBottom = async () => {
|
||||||
@ -84,31 +87,19 @@ const scrollToBottom = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 发送文本
|
// 发送文本
|
||||||
function sendText() {
|
async function sendText() {
|
||||||
if (!input.value.trim()) return;
|
if (!input.value.trim()) return;
|
||||||
|
// 根据 C# MessageBaseDto 构造的示例对象
|
||||||
const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
const msg = {
|
||||||
const newMessage = {
|
type: "Text", // 消息类型,例如 'Text', 'Image', 'File'
|
||||||
id: Date.now(),
|
chatType: "PRIVATE", // 'PRIVATE' 或 'GROUP'
|
||||||
type: 'text',
|
senderId: conversationInfo.value.userId, // 当前用户ID (对应 int)
|
||||||
|
receiverId: conversationInfo.value.targetId, // 接收者ID (对应 int)
|
||||||
content: input.value,
|
content: input.value,
|
||||||
mine: true,
|
timeStamp: new Date().toISOString() // 对应 DateTime,建议存标准 ISO 字符串
|
||||||
time: now
|
|
||||||
};
|
};
|
||||||
|
await signalRStore.sendMsg(msg);
|
||||||
// 插入消息
|
|
||||||
if (!messages.value[props.id]) {
|
|
||||||
messages.value[props.id] = [];
|
|
||||||
}
|
|
||||||
messages.value[props.id].push(newMessage);
|
|
||||||
|
|
||||||
// 同步更新会话列表最后一条摘要
|
|
||||||
if (currentSession.value) {
|
|
||||||
currentSession.value.last = input.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.value = ''; // 清空输入框
|
input.value = ''; // 清空输入框
|
||||||
scrollToBottom(); // 滚动
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通话模拟
|
// 通话模拟
|
||||||
@ -140,7 +131,8 @@ async function loadMessages(conversationId, msgId = null, pageSize = null) {
|
|||||||
// 初始化时滚动到底部
|
// 初始化时滚动到底部
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadConversation(props.id);
|
await loadConversation(props.id);
|
||||||
await loadMessages(props.id);
|
const sessionid = generateSessionId(conversationInfo.userId, conversationInfo.targetId)
|
||||||
|
await chatStore.swtichSession(sessionid,props.id);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -49,7 +49,6 @@ const currentSession = computed(() => sessions.value.find(s => s.id === activeId
|
|||||||
|
|
||||||
function selectSession(s) {
|
function selectSession(s) {
|
||||||
activeId.value = s.id
|
activeId.value = s.id
|
||||||
s.unread = 0
|
|
||||||
router.push(`/messages/chat/${s.id}`)
|
router.push(`/messages/chat/${s.id}`)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user