diff --git a/backend/IM_API/Configs/ServiceCollectionExtensions.cs b/backend/IM_API/Configs/ServiceCollectionExtensions.cs index bbb43c4..f811e2e 100644 --- a/backend/IM_API/Configs/ServiceCollectionExtensions.cs +++ b/backend/IM_API/Configs/ServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ namespace IM_API.Configs services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); return services; diff --git a/backend/IM_API/Controllers/ConversationController.cs b/backend/IM_API/Controllers/ConversationController.cs new file mode 100644 index 0000000..77db965 --- /dev/null +++ b/backend/IM_API/Controllers/ConversationController.cs @@ -0,0 +1,33 @@ +using IM_API.Dtos; +using IM_API.Interface.Services; +using IM_API.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace IM_API.Controllers +{ + [Route("api/[controller]/[action]")] + [Authorize] + [ApiController] + public class ConversationController : ControllerBase + { + private readonly IConversationService _conversationSerivice; + private readonly ILogger _logger; + public ConversationController(IConversationService conversationSerivice, ILogger logger) + { + _conversationSerivice = conversationSerivice; + _logger = logger; + } + [HttpGet] + public async Task List() + { + var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); + var list = await _conversationSerivice.GetConversationsAsync(int.Parse(userIdStr)); + var res = new BaseResponse>(list); + return Ok(res); + } + + } +} diff --git a/backend/IM_API/Interface/Services/IConversationService.cs b/backend/IM_API/Interface/Services/IConversationService.cs index 9c4bf63..3f2aa5a 100644 --- a/backend/IM_API/Interface/Services/IConversationService.cs +++ b/backend/IM_API/Interface/Services/IConversationService.cs @@ -10,12 +10,18 @@ namespace IM_API.Interface.Services /// /// /// - Task ClearConversationsAsync(ClearConversationsDto clearConversationsDto); + Task ClearConversationsAsync(int userId); + /// + /// 删除单个聊天会话 + /// + /// + /// + Task DeleteConversationAsync(int conversationId); /// /// 获取用户当前消息会话 /// /// 用户id /// - Task GetConversationsAsync(int userId); + Task> GetConversationsAsync(int userId); } } diff --git a/backend/IM_API/Services/ConversationService.cs b/backend/IM_API/Services/ConversationService.cs new file mode 100644 index 0000000..56632a1 --- /dev/null +++ b/backend/IM_API/Services/ConversationService.cs @@ -0,0 +1,44 @@ +using IM_API.Dtos; +using IM_API.Exceptions; +using IM_API.Interface.Services; +using IM_API.Models; +using IM_API.Tools; +using Microsoft.EntityFrameworkCore; + +namespace IM_API.Services +{ + public class ConversationService : IConversationService + { + private readonly ImContext _context; + public ConversationService(ImContext context) + { + _context = context; + } + #region 删除用户会话 + public async Task ClearConversationsAsync(int userId) + { + await _context.Conversations.Where(x => x.UserId == userId).ExecuteDeleteAsync(); + return true; + } + + + #endregion + #region 获取用户会话列表 + public async Task> GetConversationsAsync(int userId) + { + return await _context.Conversations.Where(x => x.UserId == userId) + .ToListAsync(); + } + #endregion + #region 删除单个会话 + public async Task DeleteConversationAsync(int conversationId) + { + var conversation = await _context.Conversations.FirstOrDefaultAsync(x => x.Id == conversationId); + if (conversation == null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND); + _context.Conversations.Remove(conversation); + await _context.SaveChangesAsync(); + return true; + } + #endregion + } +} \ No newline at end of file diff --git a/backend/IM_API/Services/FriendService.cs b/backend/IM_API/Services/FriendService.cs index 6576b36..e5ef1f1 100644 --- a/backend/IM_API/Services/FriendService.cs +++ b/backend/IM_API/Services/FriendService.cs @@ -68,7 +68,7 @@ namespace IM_API.Services { query = query.OrderByDescending(x => x.UserId); } - var friendList = await query.Skip((page - 1 * limit)).Take(limit).ToListAsync(); + var friendList = await query.Skip(((page - 1) * limit)).Take(limit).ToListAsync(); return _mapper.Map>(friendList); } #endregion diff --git a/backend/IM_API/Services/MessageService.cs b/backend/IM_API/Services/MessageService.cs index d3e8b16..223c775 100644 --- a/backend/IM_API/Services/MessageService.cs +++ b/backend/IM_API/Services/MessageService.cs @@ -54,12 +54,24 @@ namespace IM_API.Services { throw new NotImplementedException(); } - - public Task SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto) + #region 发送群消息 + public async Task SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto) { - throw new NotImplementedException(); - } + //判断群存在 + var isExist = await _context.Groups.AnyAsync(x => x.Id == groupId); + if (!isExist) throw new BaseException(CodeDefine.GROUP_NOT_FOUND); + //判断是否是群成员 + var isMember = await _context.GroupMembers.AnyAsync(x => x.GroupId == groupId && x.UserId == senderId); + if (!isMember) throw new BaseException(CodeDefine.NO_GROUP_PERMISSION); + var message = _mapper.Map(dto); + message.Sender = senderId; + _context.Messages.Add(message); + await _context.SaveChangesAsync(); + return true; + } + #endregion + #region 发送私聊消息 public async Task SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto) { bool isExist = await _context.Friends.AnyAsync(x => x.FriendId == receiverId); @@ -70,5 +82,6 @@ namespace IM_API.Services await _context.SaveChangesAsync(); return true; } + #endregion } } diff --git a/backend/IM_API/Tools/CodeDefine.cs b/backend/IM_API/Tools/CodeDefine.cs index 5733b58..e62989e 100644 --- a/backend/IM_API/Tools/CodeDefine.cs +++ b/backend/IM_API/Tools/CodeDefine.cs @@ -102,5 +102,8 @@ /// 后台日志写入失败 public static CodeDefine OPERATION_LOG_FAILED = new CodeDefine(3004, "操作记录失败"); + // 3.9 会话相关错误(3100 ~ 3199) + /// 发送时异常 + public static CodeDefine CONVERSATION_NOT_FOUND = new CodeDefine(3100, "会话不存在"); } } diff --git a/frontend/web/.env b/frontend/web/.env index ba350c4..b61e7b0 100644 --- a/frontend/web/.env +++ b/frontend/web/.env @@ -1 +1 @@ -VITE_API_BASE_URL = http://192.168.5.116:7070/api \ No newline at end of file +VITE_API_BASE_URL = http://localhost:5202/api \ No newline at end of file diff --git a/frontend/web/src/assets/default_avatar.png b/frontend/web/src/assets/default_avatar.png new file mode 100644 index 0000000..a21d544 Binary files /dev/null and b/frontend/web/src/assets/default_avatar.png differ diff --git a/frontend/web/src/router/index.js b/frontend/web/src/router/index.js index e966e63..e39fea8 100644 --- a/frontend/web/src/router/index.js +++ b/frontend/web/src/router/index.js @@ -11,6 +11,23 @@ const routes = [ { path: '/', component: MainView, + children: [ + { + path: '/messages', + name: 'userMessages', + component: () => import('@/views/messages/MessageList.vue'), + children: [ + { + path: '/messages/chat/:id', + name: '/msgChat', + component: () => import('@/views/messages/MessageContent.vue'), + props: true + } + ] + }, + { path: '/contacts', name: 'userContacts', component: () => import('@/views/contact/ContactList.vue') }, + { path: '/settings', name: 'userSettings', component: () => import('@/views/settings/SettingMenu.vue') } + ], meta: { requiresAuth: true } diff --git a/frontend/web/src/services/api.js b/frontend/web/src/services/api.js index a1fa558..dc5fd77 100644 --- a/frontend/web/src/services/api.js +++ b/frontend/web/src/services/api.js @@ -1,8 +1,10 @@ import axios from 'axios' import { useMessage } from '@/components/messages/useAlert'; import router from '@/router'; +import { useAuthStore } from '@/stores/auth'; -const message = useMessage() +const message = useMessage(); +const authStore = useAuthStore(); const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', // 从环境变量中读取基础 URL @@ -14,7 +16,7 @@ const api = axios.create({ api.interceptors.request.use( config => { - const token = localStorage.getItem('authToken'); + const token = authStore.token; if (token) { config.headers.Authorization = `Bearer ${token}`; } diff --git a/frontend/web/src/services/auth.js b/frontend/web/src/services/auth.js index 0df0456..e3f62e5 100644 --- a/frontend/web/src/services/auth.js +++ b/frontend/web/src/services/auth.js @@ -7,5 +7,11 @@ export const authService = { * @returns */ login: (data) => request.post('/auth/login', data), + + /** + * 用户注册 + * @param {*} data + * @returns + */ register: (data) => request.post('/auth/register', data) } \ No newline at end of file diff --git a/frontend/web/src/services/friend.js b/frontend/web/src/services/friend.js new file mode 100644 index 0000000..c4dc0f2 --- /dev/null +++ b/frontend/web/src/services/friend.js @@ -0,0 +1,14 @@ +import { request } from "./api"; + +export const friendService = { + + /** + * 获取好友列表 + * @param {*} page 当前页 + * @param {*} limit 页大小 + * @returns + */ + getFriendList: (page = 1, limit = 100) => request.get(`/friend/list?page=${page}&limit=${limit}`) + + +} \ No newline at end of file diff --git a/frontend/web/src/stores/auth.js b/frontend/web/src/stores/auth.js index 093b0a1..2193a06 100644 --- a/frontend/web/src/stores/auth.js +++ b/frontend/web/src/stores/auth.js @@ -3,7 +3,8 @@ import { defineStore } from 'pinia' export const useAuthStore = defineStore('auth', () => { const token = ref(localStorage.getItem('user_token') || ''); - const userInfo = ref(null); + const refreshToken = ref(localStorage.getItem('refresh_token') || ''); + const userInfo = ref(localStorage.getItem('user_info') || ''); //判断是否已登录 const isLoggedIn = computed(() => !!token.value); @@ -13,10 +14,13 @@ export const useAuthStore = defineStore('auth', () => { * @param {String} newToken 用户凭证 * @param {*} user 用户信息 */ - function setLoginInfo(newToken, user) { + function setLoginInfo(newToken, newRefreshToken, user) { token.value = newToken; + refreshToken.value = newRefreshToken userInfo.value = user; localStorage.setItem('user_token', newToken); + localStorage.setItem('refresh_token', refreshToken) + localStorage.setItem('user_info', user) } /** @@ -26,6 +30,8 @@ export const useAuthStore = defineStore('auth', () => { token.value = ''; userInfo.value = null; localStorage.removeItem('user_token'); + localStorage.removeItem('refresh_token') + localStorage.removeItem('user_info') } return { token, userInfo, isLoggedIn, setLoginInfo, logout }; diff --git a/frontend/web/src/views/Main.vue b/frontend/web/src/views/Main.vue index ab5d6b7..0036541 100644 --- a/frontend/web/src/views/Main.vue +++ b/frontend/web/src/views/Main.vue @@ -1,477 +1,226 @@ + +.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; } + \ No newline at end of file diff --git a/frontend/web/src/views/auth/Login.vue b/frontend/web/src/views/auth/Login.vue index ab81d33..97f7b57 100644 --- a/frontend/web/src/views/auth/Login.vue +++ b/frontend/web/src/views/auth/Login.vue @@ -87,7 +87,7 @@ const handleLogin = async () => { const res = await authService.login(form); if(res.code === 0){ message.success('登陆成功。') - authStore.setLoginInfo(res.data.token, res.data.userInfo); + authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo); loading.value = false; router.push('/index') }else{ diff --git a/frontend/web/src/views/contact/ContactList.vue b/frontend/web/src/views/contact/ContactList.vue new file mode 100644 index 0000000..8a09534 --- /dev/null +++ b/frontend/web/src/views/contact/ContactList.vue @@ -0,0 +1,327 @@ + + + + + \ No newline at end of file diff --git a/frontend/web/src/views/contact/UserInfoContent.vue b/frontend/web/src/views/contact/UserInfoContent.vue new file mode 100644 index 0000000..41a40c8 --- /dev/null +++ b/frontend/web/src/views/contact/UserInfoContent.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/web/src/views/messages/MessageContent.vue b/frontend/web/src/views/messages/MessageContent.vue new file mode 100644 index 0000000..8b5293a --- /dev/null +++ b/frontend/web/src/views/messages/MessageContent.vue @@ -0,0 +1,234 @@ + + + + + \ No newline at end of file diff --git a/frontend/web/src/views/messages/MessageList.vue b/frontend/web/src/views/messages/MessageList.vue new file mode 100644 index 0000000..820d049 --- /dev/null +++ b/frontend/web/src/views/messages/MessageList.vue @@ -0,0 +1,245 @@ + + + + + \ No newline at end of file diff --git a/frontend/web/src/views/settings/SettingMenu.vue b/frontend/web/src/views/settings/SettingMenu.vue new file mode 100644 index 0000000..41a40c8 --- /dev/null +++ b/frontend/web/src/views/settings/SettingMenu.vue @@ -0,0 +1 @@ + \ No newline at end of file