From a31735dbe252370859509d7e008897903de02c4a Mon Sep 17 00:00:00 2001 From: nanxun Date: Wed, 21 Jan 2026 22:30:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E5=B7=B2=E7=9F=A5=E9=97=AE=E9=A2=98=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=EF=BC=9A=20=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E5=B7=B2=E7=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/IM_API/Configs/MapperConfig.cs | 12 +- .../IM_API/Controllers/FriendController.cs | 6 +- backend/IM_API/Dtos/BaseResponse.cs | 6 +- backend/IM_API/Dtos/FriendRequestDto.cs | 2 +- backend/IM_API/Dtos/FriendRequestResDto.cs | 36 ++ .../Interface/Services/IFriendSerivce.cs | 2 +- backend/IM_API/Services/FriendService.cs | 49 ++- .../web/src/components/user/RequestFriend.vue | 0 .../web/src/components/user/SearchUser.vue | 399 ++++++++++++++++++ frontend/web/src/handler/messageHandler.js | 1 - frontend/web/src/router/index.js | 25 +- frontend/web/src/services/friend.js | 22 +- .../src/services/useBrowserNotification.js | 21 + frontend/web/src/stores/chat.js | 1 + frontend/web/src/stores/contact.js | 45 ++ frontend/web/src/stores/signalr.js | 12 +- frontend/web/src/utils/db/baseDb.js | 9 +- frontend/web/src/utils/db/contactDB.js | 18 + frontend/web/src/utils/db/messageDB.js | 21 +- frontend/web/src/views/Test.vue | 368 ++++++++-------- .../web/src/views/contact/ContactDefault.vue | 136 ++++++ .../web/src/views/contact/ContactList.vue | 219 ++-------- .../src/views/contact/FriendRequestList.vue | 200 +++++++++ .../web/src/views/contact/UserInfoContent.vue | 200 ++++++++- .../web/src/views/messages/MessageList.vue | 41 +- 25 files changed, 1418 insertions(+), 433 deletions(-) create mode 100644 backend/IM_API/Dtos/FriendRequestResDto.cs create mode 100644 frontend/web/src/components/user/RequestFriend.vue create mode 100644 frontend/web/src/components/user/SearchUser.vue create mode 100644 frontend/web/src/services/useBrowserNotification.js create mode 100644 frontend/web/src/stores/contact.js create mode 100644 frontend/web/src/utils/db/contactDB.js create mode 100644 frontend/web/src/views/contact/ContactDefault.vue create mode 100644 frontend/web/src/views/contact/FriendRequestList.vue diff --git a/backend/IM_API/Configs/MapperConfig.cs b/backend/IM_API/Configs/MapperConfig.cs index 5db0450..beab767 100644 --- a/backend/IM_API/Configs/MapperConfig.cs +++ b/backend/IM_API/Configs/MapperConfig.cs @@ -30,12 +30,12 @@ namespace IM_API.Configs .ForMember(dest => dest.UserInfo, opt => opt.MapFrom(src => src.FriendNavigation)) ; //好友请求通过后新增好友关系 - CreateMap() - .ForMember(dest => dest.UserId , opt => opt.MapFrom(src => src.RequestUser)) - .ForMember(dest => dest.FriendId , opt => opt.MapFrom(src => src.ResponseUser)) - .ForMember(dest => dest.StatusEnum , opt =>opt.MapFrom(src => FriendStatus.Added)) - .ForMember(dest => dest.RemarkName , opt => opt.MapFrom(src => src.ResponseUserNavigation.NickName)) - .ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.Now)) + CreateMap() + .ForMember(dest => dest.UserId , opt => opt.MapFrom(src => src.FromUserId)) + .ForMember(dest => dest.FriendId , opt => opt.MapFrom(src => src.ToUserId)) + .ForMember(dest => dest.StatusEnum , opt =>opt.MapFrom(src => FriendStatus.Pending)) + .ForMember(dest => dest.RemarkName , opt => opt.MapFrom(src => src.RemarkName)) + .ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.UtcNow)) ; //发起好友请求转换请求对象 CreateMap() diff --git a/backend/IM_API/Controllers/FriendController.cs b/backend/IM_API/Controllers/FriendController.cs index 477f5c7..01a7f57 100644 --- a/backend/IM_API/Controllers/FriendController.cs +++ b/backend/IM_API/Controllers/FriendController.cs @@ -44,12 +44,12 @@ namespace IM_API.Controllers /// /// [HttpGet] - public async Task Requests(bool isReceived,int page,int limit,bool desc) + public async Task Requests(int page,int limit,bool desc) { var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); int userId = int.Parse(userIdStr); - var list = await _friendService.GetFriendRequestListAsync(userId,isReceived,page,limit,desc); - var res = new BaseResponse>(list); + var list = await _friendService.GetFriendRequestListAsync(userId,page,limit,desc); + var res = new BaseResponse>(list); return Ok(res); } /// diff --git a/backend/IM_API/Dtos/BaseResponse.cs b/backend/IM_API/Dtos/BaseResponse.cs index 12a0501..fb34f0f 100644 --- a/backend/IM_API/Dtos/BaseResponse.cs +++ b/backend/IM_API/Dtos/BaseResponse.cs @@ -73,6 +73,10 @@ namespace IM_API.Dtos this.Code = codeDefine.Code; this.Message = codeDefine.Message; } - public BaseResponse() { } + public BaseResponse() + { + this.Code = CodeDefine.SUCCESS.Code; + this.Message = CodeDefine.SUCCESS.Message; + } } } diff --git a/backend/IM_API/Dtos/FriendRequestDto.cs b/backend/IM_API/Dtos/FriendRequestDto.cs index 34460a4..106117b 100644 --- a/backend/IM_API/Dtos/FriendRequestDto.cs +++ b/backend/IM_API/Dtos/FriendRequestDto.cs @@ -4,7 +4,7 @@ namespace IM_API.Dtos { public class FriendRequestDto { - public int FromUserId { get; set; } + public int? FromUserId { get; set; } public int ToUserId { get; set; } [Required(ErrorMessage = "备注名必填")] [StringLength(20, ErrorMessage = "备注名不能超过20位字符")] diff --git a/backend/IM_API/Dtos/FriendRequestResDto.cs b/backend/IM_API/Dtos/FriendRequestResDto.cs new file mode 100644 index 0000000..1fbf76d --- /dev/null +++ b/backend/IM_API/Dtos/FriendRequestResDto.cs @@ -0,0 +1,36 @@ +using IM_API.Models; + +namespace IM_API.Dtos +{ + public class FriendRequestResDto + { + public int Id { get; set; } + + /// + /// 申请人 + /// + public int RequestUser { get; set; } + + /// + /// 被申请人 + /// + public int ResponseUser { get; set; } + public string Avatar { get; set; } + public string NickName { get; set; } + + /// + /// 申请时间 + /// + public DateTime Created { get; set; } + + /// + /// 申请附言 + /// + public string? Description { get; set; } + + /// + /// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) + /// + public FriendRequestState State { get; set; } + } +} diff --git a/backend/IM_API/Interface/Services/IFriendSerivce.cs b/backend/IM_API/Interface/Services/IFriendSerivce.cs index 804cb73..16aa4c5 100644 --- a/backend/IM_API/Interface/Services/IFriendSerivce.cs +++ b/backend/IM_API/Interface/Services/IFriendSerivce.cs @@ -27,7 +27,7 @@ namespace IM_API.Interface.Services /// /// /// - Task> GetFriendRequestListAsync(int userId,bool isReceived,int page,int limit, bool desc); + Task> GetFriendRequestListAsync(int userId,int page,int limit, bool desc); /// /// 处理好友请求 /// diff --git a/backend/IM_API/Services/FriendService.cs b/backend/IM_API/Services/FriendService.cs index 206f699..9e6cd63 100644 --- a/backend/IM_API/Services/FriendService.cs +++ b/backend/IM_API/Services/FriendService.cs @@ -73,23 +73,29 @@ namespace IM_API.Services } #endregion #region 获取好友请求列表 - public async Task> GetFriendRequestListAsync(int userId, bool isReceived, int page, int limit, bool desc) + public async Task> GetFriendRequestListAsync(int userId, int page, int limit, bool desc) { - var query = _context.FriendRequests.AsQueryable(); - //是否为请求方 - if (isReceived) - { - query = _context.FriendRequests.Where(x => x.ResponseUser == userId); - } - else - { - query = _context.FriendRequests.Where(x => x.RequestUser == userId); - } - if (desc) - { - query = query.OrderByDescending(x => x.Id); - } - var friendRequestList = await query.Skip((page - 1 * limit)).Take(limit).ToListAsync(); + var query = _context.FriendRequests + .Include(x => x.ResponseUserNavigation) + .Include(x => x.RequestUserNavigation) + .Where( + x => (x.ResponseUser == userId) || + x.RequestUser == userId + ) + .Select(s => new FriendRequestResDto + { + Id = s.Id, + RequestUser = s.RequestUser, + ResponseUser = s.ResponseUser, + Avatar = s.RequestUser == userId ? s.ResponseUserNavigation.Avatar : s.RequestUserNavigation.Avatar, + Created = s.Created, + NickName = s.RequestUser == userId ? s.ResponseUserNavigation.NickName : s.RequestUserNavigation.NickName, + Description = s.Description, + State = (FriendRequestState)s.State + }) + ; + query = query.OrderByDescending(x => x.Id); + var friendRequestList = await query.Skip(((page - 1) * limit)).Take(limit).ToListAsync(); return friendRequestList; } #endregion @@ -154,16 +160,17 @@ namespace IM_API.Services if (alreadyExists) throw new BaseException(CodeDefine.FRIEND_REQUEST_EXISTS); + var friendShip = await _context.Friends.FirstOrDefaultAsync(x => x.UserId == dto.FromUserId && x.FriendId == dto.ToUserId); + //检查是否被对方拉黑 - bool isBlocked = await _context.Friends.AnyAsync(x => - x.UserId == dto.FromUserId && x.FriendId == dto.ToUserId && x.Status == (sbyte)FriendStatus.Blocked - ); + bool isBlocked = friendShip != null && friendShip.StatusEnum == FriendStatus.Blocked; if (isBlocked) throw new BaseException(CodeDefine.FRIEND_REQUEST_REJECTED); - + if (friendShip != null) + throw new BaseException(CodeDefine.ALREADY_FRIENDS); //生成实体 var friendRequst = _mapper.Map(dto); - var friend = _mapper.Map(friendRequst); + var friend = _mapper.Map(dto); _context.FriendRequests.Add(friendRequst); _context.Friends.Add(friend); await _context.SaveChangesAsync(); diff --git a/frontend/web/src/components/user/RequestFriend.vue b/frontend/web/src/components/user/RequestFriend.vue new file mode 100644 index 0000000..e69de29 diff --git a/frontend/web/src/components/user/SearchUser.vue b/frontend/web/src/components/user/SearchUser.vue new file mode 100644 index 0000000..2312c39 --- /dev/null +++ b/frontend/web/src/components/user/SearchUser.vue @@ -0,0 +1,399 @@ + + + + + \ No newline at end of file diff --git a/frontend/web/src/handler/messageHandler.js b/frontend/web/src/handler/messageHandler.js index 3101816..484f53e 100644 --- a/frontend/web/src/handler/messageHandler.js +++ b/frontend/web/src/handler/messageHandler.js @@ -9,7 +9,6 @@ export const messageHandler = (msg) => { } else { conversation.unreadCount += 1; } - console.log(conversation) conversation.dateTime = new Date().toISOString(); } \ No newline at end of file diff --git a/frontend/web/src/router/index.js b/frontend/web/src/router/index.js index a4fa55d..363ec76 100644 --- a/frontend/web/src/router/index.js +++ b/frontend/web/src/router/index.js @@ -35,7 +35,30 @@ const routes = [ } ] }, - { path: '/contacts', name: 'userContacts', component: () => import('@/views/contact/ContactList.vue') }, + { + path: '/contacts', + name: 'userContacts', + redirect: '/contacts/index', + component: () => import('@/views/contact/ContactList.vue'), + children: [ + { + path: '/contacts/index', + name: "contactDefault", + component: () => import('@/views/contact/ContactDefault.vue') + }, + { + path: '/contacts/info/:id', + name: 'contactInfo', + component: () => import('@/views/contact/UserInfoContent.vue'), + props: true + }, + { + path: '/contacts/requests', + name: 'friendRequests', + component: () => import('@/views/contact/FriendRequestList.vue') + } + ] + }, { path: '/settings', name: 'userSettings', component: () => import('@/views/settings/SettingMenu.vue') } ], meta: { diff --git a/frontend/web/src/services/friend.js b/frontend/web/src/services/friend.js index c4dc0f2..90b8788 100644 --- a/frontend/web/src/services/friend.js +++ b/frontend/web/src/services/friend.js @@ -8,7 +8,25 @@ export const friendService = { * @param {*} limit 页大小 * @returns */ - getFriendList: (page = 1, limit = 100) => request.get(`/friend/list?page=${page}&limit=${limit}`) - + getFriendList: (page = 1, limit = 100) => request.get(`/friend/list?page=${page}&limit=${limit}`), + /** + * 搜索好友 + * @param {*} username + * @returns + */ + findUser: (username) => request.get(`/user/findbyusername?username=${username}`), + /** + * 申请添加好友 + * @param {*} params + * @returns + */ + requestFriend: (params) => request.post('/friend/request', params), + /** + * 获取好友请求列表 + * @param {*} page + * @param {*} limit + * @returns + */ + getFriendRequests: (page = 1, limit = 100) => request.get(`/friend/requests?page=${page}&limit=${limit}`) } \ No newline at end of file diff --git a/frontend/web/src/services/useBrowserNotification.js b/frontend/web/src/services/useBrowserNotification.js new file mode 100644 index 0000000..d97a58c --- /dev/null +++ b/frontend/web/src/services/useBrowserNotification.js @@ -0,0 +1,21 @@ +export function useBrowserNotification() { + const requestPermission = async () => { + if ("Notification" in window && Notification.permission === "default") { + await Notification.requestPermission(); + } + }; + + const send = (title, options = {}) => { + if ("Notification" in window && Notification.permission === "granted") { + // 如果页面正处于激活状态,通常不需要弹窗提醒,以免干扰用户 + /* + if (document.visibilityState === 'visible' && document.hasFocus()) { + return; + } + */ + return new Notification(title, options); + } + }; + + return { requestPermission, send }; +} \ No newline at end of file diff --git a/frontend/web/src/stores/chat.js b/frontend/web/src/stores/chat.js index 8c22dee..c061e4f 100644 --- a/frontend/web/src/stores/chat.js +++ b/frontend/web/src/stores/chat.js @@ -36,6 +36,7 @@ export const useChatStore = defineStore('chat', { this.messages = []; //先从浏览器缓存加载一部分消息列表 const localHistory = await messagesDb.getPageMessages(sessionId, new Date().toISOString(), this.pageSize); + console.log(localHistory) if (localHistory.length > 0) { this.messages = localHistory; } else { diff --git a/frontend/web/src/stores/contact.js b/frontend/web/src/stores/contact.js new file mode 100644 index 0000000..50c39ba --- /dev/null +++ b/frontend/web/src/stores/contact.js @@ -0,0 +1,45 @@ +import { defineStore } from "pinia"; +import { contactDb } from "@/utils/db/contactDB"; +import { friendService } from "@/services/friend"; +import { useMessage } from "@/components/messages/useAlert"; + +export const useContactStore = defineStore('contact', { + state: () => ({ + contacts: [], + + }), + actions: { + async addContact(contact) { + this.contacts.push(contact); + await contactDb.save(contact); + }, + async loadContactList() { + if (this.contacts.length == 0) { + this.contacts = await contactDb.getAll(); + } + this.fetchContactFromServer(); + }, + async fetchContactFromServer() { + const message = useMessage(); + const res = await friendService.getFriendList(); + if (res.code == 0) { + const localMap = new Map(this.contacts.map(item => [item.id, item])); + res.data.forEach(item => { + const existingItem = localMap.get(item.id); + if (existingItem) { + // --- 局部更新 --- + // 使用 Object.assign 将新数据合并到旧对象上,保持响应式引用 + Object.assign(existingItem, item); + } else { + // --- 插入新会话 --- + this.contacts.push(item); + } + // 同步到本地数据库 + contactDb.save(item); + }); + } else { + message.error(res.message); + } + } + } +}) \ No newline at end of file diff --git a/frontend/web/src/stores/signalr.js b/frontend/web/src/stores/signalr.js index 1392db2..a5a2288 100644 --- a/frontend/web/src/stores/signalr.js +++ b/frontend/web/src/stores/signalr.js @@ -6,6 +6,8 @@ import { useChatStore } from "./chat"; import { authService } from "@/services/auth"; import { generateSessionId } from "@/utils/sessionIdTools"; import { messageHandler } from "@/handler/messageHandler"; +import { useBrowserNotification } from "@/services/useBrowserNotification"; +import { useConversationStore } from "./conversation"; export const useSignalRStore = defineStore('signalr', { state: () => ({ @@ -40,9 +42,17 @@ export const useSignalRStore = defineStore('signalr', { }, registerHandlers() { const chatStore = useChatStore() + const browserNotification = useBrowserNotification(); + this.connection.on('ReceiveMessage', (msg) => { + const sessionId = generateSessionId(msg.senderId, msg.receiverId); messageHandler(msg); - chatStore.addMessage(msg); + chatStore.addMessage(msg, sessionId); + const conversation = useConversationStore().conversations.find(x => x.targetId == msg.senderId); + browserNotification.send(`${conversation.targetName}发来一条消息`, { + body: msg.content, + icon: conversation.targetAvatar + }); }); this.connection.onclose(() => { this.isConnected = false }); diff --git a/frontend/web/src/utils/db/baseDb.js b/frontend/web/src/utils/db/baseDb.js index 9768851..b28674a 100644 --- a/frontend/web/src/utils/db/baseDb.js +++ b/frontend/web/src/utils/db/baseDb.js @@ -3,17 +3,24 @@ import { openDB } from "idb"; const DBNAME = 'IM_DB'; const STORE_NAME = 'messages'; const CONVERSARION_STORE_NAME = 'conversations'; +const CONTACT_STORE_NAME = 'contacts'; -export const dbPromise = openDB(DBNAME, 2, { +export const dbPromise = openDB(DBNAME, 4, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'msgId' }); store.createIndex('by-sessionId', 'sessionId'); store.createIndex('by-time', 'timeStamp'); + store.createIndex('by-session-time', ['sessionId', 'timeStamp']); } if (!db.objectStoreNames.contains(CONVERSARION_STORE_NAME)) { const store = db.createObjectStore(CONVERSARION_STORE_NAME, { keyPath: 'id' }); store.createIndex('by-id', 'id'); } + if (!db.objectStoreNames.contains(CONTACT_STORE_NAME)) { + const store = db.createObjectStore(CONTACT_STORE_NAME, { keyPath: 'id' }); + store.createIndex('by-id', 'id'); + store.createIndex('by-username', 'username'); + } } }) \ No newline at end of file diff --git a/frontend/web/src/utils/db/contactDB.js b/frontend/web/src/utils/db/contactDB.js new file mode 100644 index 0000000..6e25378 --- /dev/null +++ b/frontend/web/src/utils/db/contactDB.js @@ -0,0 +1,18 @@ +import { dbPromise } from "./baseDb" + +const STORE_NAME = 'contacts'; + +export const contactDb = { + async save(contact) { + (await dbPromise).put(STORE_NAME, contact); + }, + async getById(id) { + return (await dbPromise).getFromIndex(STORE_NAME, 'by-id', id); + }, + async getByUsername(username) { + return (await dbPromise).getFromIndex(STORE_NAME, 'by-username', username); + }, + async getAll() { + return (await dbPromise).getAll(STORE_NAME); + } +} \ No newline at end of file diff --git a/frontend/web/src/utils/db/messageDB.js b/frontend/web/src/utils/db/messageDB.js index d4eb59f..cde60bc 100644 --- a/frontend/web/src/utils/db/messageDB.js +++ b/frontend/web/src/utils/db/messageDB.js @@ -15,17 +15,26 @@ export const messagesDb = { 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); + const index = tx.store.index('by-session-time'); // 使用复合索引 + + // 定义范围:从 [sessionId, 最早时间] 到 [sessionId, beforeTimeStamp) + // 注意:IDBKeyRange.bound([sessionId, ""], [sessionId, beforeTimeStamp], false, true) + // 或者简单使用 upperbound 限制最大值 + const range = IDBKeyRange.upperBound([sessionId, beforeTimeStamp], true); + + // 'prev' 表示从最新的往回找(倒序) 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); // 插到数组开头,保持时间正序 - } + // 关键安全检查:因为 upperBound 可能会越界捞到其他 sessionId 的数据 + //(复合索引的特性决定了 sessionId 不一致的数据会排在后面) + if (cursor.value.sessionId !== sessionId) break; + + results.unshift(cursor.value); // 放入结果集开头,保证返回的是时间升序 cursor = await cursor.continue(); } + return results; } } \ No newline at end of file diff --git a/frontend/web/src/views/Test.vue b/frontend/web/src/views/Test.vue index 2ba7799..f5eb261 100644 --- a/frontend/web/src/views/Test.vue +++ b/frontend/web/src/views/Test.vue @@ -1,254 +1,232 @@ \ No newline at end of file diff --git a/frontend/web/src/views/contact/ContactDefault.vue b/frontend/web/src/views/contact/ContactDefault.vue new file mode 100644 index 0000000..f3e5567 --- /dev/null +++ b/frontend/web/src/views/contact/ContactDefault.vue @@ -0,0 +1,136 @@ + + + \ No newline at end of file diff --git a/frontend/web/src/views/contact/ContactList.vue b/frontend/web/src/views/contact/ContactList.vue index 1ffc987..56adb59 100644 --- a/frontend/web/src/views/contact/ContactList.vue +++ b/frontend/web/src/views/contact/ContactList.vue @@ -10,10 +10,10 @@
-
+
新的朋友
-
+
群聊
@@ -29,7 +29,7 @@ :key="c.id" class="list-item" :class="{active: activeContactId === c.id}" - @click="activeContactId = c.id"> + @click="routeUserInfo(c.id)">
{{ c.remarkName }}
@@ -37,49 +37,7 @@
- -
-
-
-
-

- {{ currentContact.remarkName }} - - {{ '♂' }} - -

-

账号:{{ currentContact.userInfo.username }}

-

地区:{{ '未知' }}

-
- -
- -
-
- 昵称 - {{ currentContact.userInfo.nickName }} -
-
- 个性签名 - {{ currentContact.signature || '这个家伙很懒,什么都没留下' }} -
-
- 来源 - 通过搜索账号添加 -
-
- -
- - -
-
- -
- -

请在左侧选择联系人查看详情

-
-
+
import { ref, computed, onMounted } from 'vue' -import { friendService } from '@/services/friend' import GroupChatModal from '@/components/groups/GroupChatModal.vue' import feather from 'feather-icons'; +import { useContactStore } from '@/stores/contact'; +import { useRouter } from 'vue-router'; +const router = useRouter(); const searchQuery = ref('') const activeContactId = ref(null) - -const contacts = ref([]); +const contactStore = useContactStore(); const groupModal = ref(false); @@ -108,11 +67,12 @@ const groupModal = ref(false); const filteredContacts = computed(() => { const searchKey = searchQuery.value.toString().trim(); if(!searchKey){ - return contacts.value; + return contactStore.contacts; } - return contacts.value.filter(c => { + + return contactStore.contacts.filter(c => { if (!c) return false const remark = c.remarkName || '' @@ -122,22 +82,13 @@ const filteredContacts = computed(() => { }) }) -const currentContact = computed(() => { - return contacts.value.find(c => c.id === activeContactId.value) -}) +const routeUserInfo = (id) => { + router.push(`/contacts/info/${id}`); + activeContactId.value = id; +} // 发送事件给父组件(用于切换回聊天Tab并打开会话) const emit = defineEmits(['start-chat']) -function handleGoToChat() { - if (currentContact.value) { - emit('start-chat', { ...currentContact.value }) - } -} - -const loadContactList = async (page = 1,limit = 100) => { - const res = await friendService.getFriendList(page,limit); - contacts.value = res.data; -} const showGroupList = () => { groupModal.value = true; @@ -145,7 +96,7 @@ const showGroupList = () => { onMounted(async () => { - await loadContactList(); + await contactStore.loadContactList(); }) @@ -206,6 +157,19 @@ onMounted(async () => { align-items: center; cursor: pointer; transition: background 0.2s; + text-decoration: none; /* 去除下划线 */ + color: inherit; /* 继承父元素的文本颜色 */ + outline: none; /* 去除点击时的蓝框 */ + -webkit-tap-highlight-color: transparent; /* 移动端点击高亮 */ +} + +/* 去除 hover、active 等状态的效果 */ +a:hover, +a:active, +a:focus { + text-decoration: none; + color: inherit; /* 保持颜色不变 */ + cursor: pointer; } .list-item:hover { background: #e2e2e2; } @@ -231,129 +195,4 @@ onMounted(async () => { .icon-box.orange { background: #faad14; } .icon-box.green { background: #52c41a; } .icon-box.blue { background: #1890ff; } - -/* --- 右侧名片区 --- */ -.profile-main { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - background: #f5f5f5; - min-width: 0; -} - -.profile-card { - width: 420px; - background: transparent; - padding: 20px; -} - -.profile-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding-bottom: 30px; - border-bottom: 1px solid #e7e7e7; - margin-bottom: 30px; -} - -.display-name { - font-size: 24px; - color: #000; - margin-bottom: 8px; - display: flex; - align-items: center; - gap: 8px; -} - -.gender-tag { font-size: 16px; } -.gender-tag.m { color: #1890ff; } -.gender-tag.f { color: #ff4d4f; } - -.sub-text { - font-size: 13px; - color: #888; - margin: 3px 0; -} - -.big-avatar { - width: 70px; - height: 70px; - border-radius: 6px; - object-fit: cover; -} - -.profile-body { - margin-bottom: 40px; -} - -.info-row { - display: flex; - margin-bottom: 15px; - font-size: 14px; -} - -.info-row .label { - width: 80px; - color: #999; -} - -.info-row .value { - color: #333; - flex: 1; -} - -.profile-footer { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; -} - -.btn-primary { - width: 160px; - padding: 10px; - background: #07c160; - color: #fff; - border: none; - border-radius: 4px; - font-weight: bold; - cursor: pointer; -} - -.btn-ghost { - width: 160px; - padding: 10px; - background: #fff; - border: 1px solid #e0e0e0; - color: #333; - border-radius: 4px; - cursor: pointer; -} - -.btn-primary:hover, .btn-ghost:hover { - opacity: 0.8; -} - -.empty-state { - text-align: center; - color: #ccc; -} - -.empty-logo { - font-size: 80px; - margin-bottom: 10px; - opacity: 0.2; -} -/* 4. 定义组件进场和退场的动画 */ -.fade-scale-enter-active, -.fade-scale-leave-active { - transition: all 0.3s ease; -} - -.fade-scale-enter-from, -.fade-scale-leave-to { - opacity: 0; - transform: scale(0.9) translateY(10px); -} \ No newline at end of file diff --git a/frontend/web/src/views/contact/FriendRequestList.vue b/frontend/web/src/views/contact/FriendRequestList.vue new file mode 100644 index 0000000..7915d83 --- /dev/null +++ b/frontend/web/src/views/contact/FriendRequestList.vue @@ -0,0 +1,200 @@ + + + + + \ No newline at end of file diff --git a/frontend/web/src/views/contact/UserInfoContent.vue b/frontend/web/src/views/contact/UserInfoContent.vue index 41a40c8..8cf42f0 100644 --- a/frontend/web/src/views/contact/UserInfoContent.vue +++ b/frontend/web/src/views/contact/UserInfoContent.vue @@ -1 +1,199 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/frontend/web/src/views/messages/MessageList.vue b/frontend/web/src/views/messages/MessageList.vue index 37607f4..b5df7bf 100644 --- a/frontend/web/src/views/messages/MessageList.vue +++ b/frontend/web/src/views/messages/MessageList.vue @@ -10,6 +10,10 @@
+
+ + 新消息无法通知,点我授予通知权限 +
+