vue-chat-kit 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -159,6 +159,70 @@
159
159
  >同意</el-button>
160
160
  </div>
161
161
  </div>
162
+
163
+ <!-- 群聊列表 -->
164
+ <div v-if="currentNavTab === 'groups' && config.modules.groups">
165
+ <div class="add-friend-section">
166
+ <div
167
+ class="add-friend-btn"
168
+ @click="createGroupDialogVisible = true"
169
+ >
170
+ <div class="add-friend-icon">
171
+ <el-icon class="text-white" :size="20"><Plus /></el-icon>
172
+ </div>
173
+ <span class="add-friend-text">创建群聊</span>
174
+ </div>
175
+ </div>
176
+ <div
177
+ v-for="group in filteredGroupList"
178
+ :key="group.groupId"
179
+ :class="[
180
+ 'chat-list-item',
181
+ currentSelectGroup?.groupId === group.groupId ? 'chat-list-item-active' : ''
182
+ ]"
183
+ @click="selectGroupChat(group)"
184
+ >
185
+ <div class="chat-list-avatar-wrapper">
186
+ <!-- 多方格群聊头像 -->
187
+ <div v-if="group.memberAvatars && group.memberAvatars.length > 0" class="group-avatar-grid">
188
+ <div
189
+ v-for="(member, index) in group.memberAvatars"
190
+ :key="member.username"
191
+ class="group-avatar-item"
192
+ :style="getGroupAvatarGridStyle(group.memberAvatars.length, index)"
193
+ >
194
+ <img
195
+ :src="member.avatar"
196
+ :alt="member.username"
197
+ class="group-avatar-img"
198
+ />
199
+ </div>
200
+ </div>
201
+ <!-- 单个头像作为后备 -->
202
+ <img
203
+ v-else
204
+ :src="group.avatar"
205
+ :alt="group.name"
206
+ class="chat-list-avatar"
207
+ />
208
+ </div>
209
+ <div class="chat-list-info">
210
+ <div class="chat-list-header">
211
+ <span class="chat-list-name">{{ group.name }}</span>
212
+ <span class="chat-list-time">{{ formatLastTime(group.lastTime) }}</span>
213
+ </div>
214
+ <div class="chat-list-preview">
215
+ <span class="chat-list-last-msg">{{ group.lastMsg }}</span>
216
+ <span
217
+ v-if="group.unread > 0"
218
+ class="chat-list-unread"
219
+ >
220
+ {{ group.unread > 99 ? '99+' : group.unread }}
221
+ </span>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
162
226
  </div>
163
227
  </div>
164
228
 
@@ -192,13 +256,14 @@
192
256
  </el-button>
193
257
  </div>
194
258
 
195
- <!-- 聊天窗口 -->
196
- <div v-if="currentChat" class="chat-window-area">
259
+ <!-- 聊天窗口(群聊或单聊) -->
260
+ <div v-if="currentChat || currentSelectGroup" class="chat-window-area">
197
261
  <!-- 顶部标题栏 -->
198
262
  <div class="chat-window-header">
199
263
  <div class="chat-window-title">
200
- <span class="chat-window-name">{{ currentChat.name }}</span>
264
+ <span class="chat-window-name">{{ currentSelectGroup ? currentSelectGroup.name : currentChat.name }}</span>
201
265
  <span
266
+ v-if="!currentSelectGroup"
202
267
  :class="[
203
268
  'chat-window-status',
204
269
  currentChat.online ? 'chat-window-status-online' : 'chat-window-status-offline'
@@ -206,10 +271,19 @@
206
271
  >
207
272
  {{ currentChat.online ? '在线' : '离线' }}
208
273
  </span>
274
+ <span v-if="currentSelectGroup" class="chat-window-status">
275
+ {{ groupMemberList.length }} 人
276
+ </span>
209
277
  </div>
210
278
  <div class="chat-window-actions">
211
279
  <el-icon class="chat-action-icon"><Search /></el-icon>
212
280
  <el-icon
281
+ v-if="currentSelectGroup"
282
+ class="chat-action-icon"
283
+ @click="showGroupSidebar = !showGroupSidebar"
284
+ ><MoreFilled /></el-icon>
285
+ <el-icon
286
+ v-else
213
287
  class="chat-action-icon"
214
288
  @click="showChatDetail = !showChatDetail"
215
289
  ><MoreFilled /></el-icon>
@@ -222,7 +296,7 @@
222
296
  class="chat-messages-container"
223
297
  >
224
298
  <div
225
- v-for="(msg, index) in currentMessages"
299
+ v-for="(msg, index) in currentSelectGroup ? currentGroupMessages : currentMessages"
226
300
  :key="index"
227
301
  :class="[
228
302
  'message-wrapper',
@@ -232,7 +306,7 @@
232
306
  <!-- 头像 -->
233
307
  <div class="message-avatar">
234
308
  <img
235
- :src="msg.isSelf ? myAvatar : currentChat.avatar"
309
+ :src="msg.avatar"
236
310
  class="message-avatar-img"
237
311
  />
238
312
  </div>
@@ -244,7 +318,9 @@
244
318
  msg.isSelf ? 'message-content-self' : 'message-content-other'
245
319
  ]"
246
320
  >
247
- <div v-if="!msg.isSelf" class="message-sender-name">{{ currentChat.name }}</div>
321
+ <div v-if="!msg.isSelf" class="message-sender-name">
322
+ {{ currentSelectGroup ? (msg.displayName || msg.sendUsername) : currentChat.name }}
323
+ </div>
248
324
 
249
325
  <div class="message-bubble-wrapper">
250
326
  <!-- 文本消息 -->
@@ -423,6 +499,202 @@
423
499
  </div>
424
500
  </div>
425
501
 
502
+ <!-- 群聊侧边栏 -->
503
+ <transition name="slide">
504
+ <div
505
+ v-if="showGroupSidebar && currentSelectGroup"
506
+ class="group-sidebar"
507
+ >
508
+ <!-- 关闭按钮 -->
509
+ <div class="group-sidebar-header">
510
+ <span class="group-sidebar-title">群聊信息</span>
511
+ <el-icon class="group-sidebar-close" @click="showGroupSidebar = false">
512
+ <Close />
513
+ </el-icon>
514
+ </div>
515
+
516
+ <div class="group-sidebar-content">
517
+ <!-- 群成员头像展示区 -->
518
+ <div class="group-members-avatar-section">
519
+ <div class="group-members-grid">
520
+ <div
521
+ v-for="member in groupMemberList.slice(0, 12)"
522
+ :key="member.username"
523
+ class="group-member-avatar-item"
524
+ @click="openEditMemberNick(member)"
525
+ >
526
+ <img
527
+ :src="member.avatar ? `${config.api.baseUrl}${member.avatar}` : `https://api.dicebear.com/7.x/avataaars/svg?seed=${member.username}`"
528
+ :alt="member.username"
529
+ class="group-member-avatar-small"
530
+ />
531
+ <span class="group-member-nickname">{{ member.memberNick || member.username }}</span>
532
+ </div>
533
+ <div
534
+ v-if="groupMemberList.length > 12"
535
+ class="group-member-avatar-item"
536
+ @click="inviteMemberDialogVisible = true"
537
+ >
538
+ <div class="group-member-add-icon">
539
+ <el-icon><Plus /></el-icon>
540
+ </div>
541
+ <span class="group-member-nickname">邀请</span>
542
+ </div>
543
+ <div
544
+ v-else
545
+ class="group-member-avatar-item"
546
+ @click="inviteMemberDialogVisible = true"
547
+ >
548
+ <div class="group-member-add-icon">
549
+ <el-icon><Plus /></el-icon>
550
+ </div>
551
+ <span class="group-member-nickname">邀请</span>
552
+ </div>
553
+ </div>
554
+ </div>
555
+
556
+ <!-- 分割线 -->
557
+ <div class="group-divider"></div>
558
+
559
+ <!-- 群设置项 -->
560
+ <div class="group-settings-section">
561
+ <!-- 群名称 -->
562
+ <div class="group-setting-item" v-if="currentSelectGroup?.owner === myUsername">
563
+ <span class="group-setting-label">群聊名称</span>
564
+ <div class="group-setting-value-wrapper">
565
+ <div v-if="editingFields.groupNickname" class="group-edit-wrapper">
566
+ <el-input
567
+ v-model="tempEditValues.groupNickname"
568
+ placeholder="请输入群聊名称"
569
+ @keyup.enter="handleSaveEditField('groupNickname')"
570
+ class="group-input-edit"
571
+ />
572
+ <div class="group-edit-buttons">
573
+ <el-button size="small" @click="cancelEditField('groupNickname')">取消</el-button>
574
+ <el-button size="small" type="primary" @click="handleSaveEditField('groupNickname')">保存</el-button>
575
+ </div>
576
+ </div>
577
+ <div
578
+ v-else
579
+ class="group-setting-value"
580
+ @click="startEditField('groupNickname')"
581
+ >
582
+ <span>{{ currentGroupInfo?.groupNickname || currentGroupInfo?.groupName || currentSelectGroup?.name || '' }}</span>
583
+ <el-icon><Edit /></el-icon>
584
+ </div>
585
+ </div>
586
+ </div>
587
+ <div class="group-setting-item" v-else>
588
+ <span class="group-setting-label">群聊名称</span>
589
+ <span class="group-setting-value-text">{{ currentGroupInfo?.groupNickname || currentGroupInfo?.groupName || currentSelectGroup?.name || '' }}</span>
590
+ </div>
591
+
592
+ <!-- 群公告 -->
593
+ <div class="group-setting-item" v-if="currentSelectGroup?.owner === myUsername">
594
+ <span class="group-setting-label">群公告</span>
595
+ <div class="group-setting-value-wrapper">
596
+ <div v-if="editingFields.notice" class="group-edit-wrapper">
597
+ <el-input
598
+ v-model="tempEditValues.notice"
599
+ type="textarea"
600
+ :rows="3"
601
+ placeholder="请输入群公告"
602
+ class="group-input-edit"
603
+ />
604
+ <div class="group-edit-buttons">
605
+ <el-button size="small" @click="cancelEditField('notice')">取消</el-button>
606
+ <el-button size="small" type="primary" @click="handleSaveEditField('notice')">保存</el-button>
607
+ </div>
608
+ </div>
609
+ <div
610
+ v-else
611
+ class="group-setting-value"
612
+ @click="startEditField('notice')"
613
+ >
614
+ <span class="group-setting-value-text">{{ currentGroupInfo?.notice || '暂无公告' }}</span>
615
+ <el-icon><Edit /></el-icon>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ <div class="group-setting-item" v-else>
620
+ <span class="group-setting-label">群公告</span>
621
+ <span class="group-setting-value-text">{{ currentGroupInfo?.notice || '暂无公告' }}</span>
622
+ </div>
623
+
624
+ <!-- 备注 -->
625
+ <div class="group-setting-item" v-if="currentSelectGroup?.owner === myUsername">
626
+ <span class="group-setting-label">备注</span>
627
+ <div class="group-setting-value-wrapper">
628
+ <div v-if="editingFields.remark" class="group-edit-wrapper">
629
+ <el-input
630
+ v-model="tempEditValues.remark"
631
+ placeholder="请输入备注"
632
+ @keyup.enter="handleSaveEditField('remark')"
633
+ class="group-input-edit"
634
+ />
635
+ <div class="group-edit-buttons">
636
+ <el-button size="small" @click="cancelEditField('remark')">取消</el-button>
637
+ <el-button size="small" type="primary" @click="handleSaveEditField('remark')">保存</el-button>
638
+ </div>
639
+ </div>
640
+ <div
641
+ v-else
642
+ class="group-setting-value"
643
+ @click="startEditField('remark')"
644
+ >
645
+ <span class="group-setting-value-text">{{ currentGroupInfo?.remark || '群聊的备注仅自己可见' }}</span>
646
+ <el-icon><Edit /></el-icon>
647
+ </div>
648
+ </div>
649
+ </div>
650
+ <div class="group-setting-item" v-else>
651
+ <span class="group-setting-label">备注</span>
652
+ <span class="group-setting-value-text">{{ currentGroupInfo?.remark || '群聊的备注仅自己可见' }}</span>
653
+ </div>
654
+
655
+ <!-- 我在本群的昵称 -->
656
+ <div class="group-setting-item" @click="openMyNicknameEdit">
657
+ <span class="group-setting-label">我在本群的昵称</span>
658
+ <div class="group-setting-value">
659
+ <span class="group-setting-value-text">{{ myNicknameInGroup || myUsername }}</span>
660
+ <el-icon><Edit /></el-icon>
661
+ </div>
662
+ </div>
663
+
664
+ <div class="group-divider"></div>
665
+
666
+ <!-- 查找聊天内容 -->
667
+ <div class="group-setting-item">
668
+ <span class="group-setting-label">查找聊天内容</span>
669
+ <el-icon><ArrowRight /></el-icon>
670
+ </div>
671
+
672
+ <!-- 消息免打扰 -->
673
+ <div class="group-setting-item">
674
+ <span class="group-setting-label">消息免打扰</span>
675
+ <el-switch v-model="muteGroup" />
676
+ </div>
677
+
678
+ <div class="group-divider"></div>
679
+
680
+ <!-- 群主专属功能 -->
681
+ <template v-if="currentSelectGroup.owner === myUsername">
682
+ <div class="group-setting-item danger" @click="handleDeleteGroup">
683
+ <span class="group-setting-label">解散群聊</span>
684
+ </div>
685
+ </template>
686
+
687
+ <!-- 普通成员退出 -->
688
+ <template v-else>
689
+ <div class="group-setting-item danger" @click="handleQuitGroup">
690
+ <span class="group-setting-label">删除并退出</span>
691
+ </div>
692
+ </template>
693
+ </div>
694
+ </div>
695
+ </div>
696
+ </transition>
697
+
426
698
  <!-- 右侧详情面板 -->
427
699
  <div
428
700
  v-if="showChatDetail"
@@ -485,6 +757,286 @@
485
757
  </div>
486
758
  </el-dialog>
487
759
 
760
+ <!-- 创建群聊弹窗 -->
761
+ <el-dialog
762
+ v-model="createGroupDialogVisible"
763
+ title="创建群聊"
764
+ width="600px"
765
+ append-to-body
766
+ >
767
+ <div class="create-group-form">
768
+ <div class="form-item">
769
+ <label class="form-label">群名称</label>
770
+ <el-input
771
+ v-model="newGroupName"
772
+ placeholder="请输入群名称"
773
+ maxlength="50"
774
+ />
775
+ </div>
776
+ <div class="form-item">
777
+ <label class="form-label">群备注</label>
778
+ <el-input
779
+ v-model="newGroupRemark"
780
+ type="textarea"
781
+ placeholder="请输入群备注(可选)"
782
+ :rows="3"
783
+ maxlength="200"
784
+ />
785
+ </div>
786
+ <div class="form-item">
787
+ <label class="form-label">选择成员</label>
788
+ <div class="member-selection">
789
+ <el-empty v-if="filteredFriendList.length === 0" description="暂无好友可以添加" />
790
+ <div
791
+ v-else
792
+ class="member-list"
793
+ >
794
+ <div
795
+ v-for="friend in filteredFriendList"
796
+ :key="friend.id"
797
+ :class="[
798
+ 'member-item',
799
+ selectedMembersForCreate.some(m => m.id === friend.id) ? 'member-selected' : ''
800
+ ]"
801
+ @click="toggleMemberForCreate(friend)"
802
+ >
803
+ <img
804
+ :src="friend.avatar"
805
+ :alt="friend.name"
806
+ class="member-avatar"
807
+ />
808
+ <span class="member-name">{{ friend.name }}</span>
809
+ <el-icon v-if="selectedMembersForCreate.some(m => m.id === friend.id)" class="member-check"><CircleCheck /></el-icon>
810
+ </div>
811
+ </div>
812
+ </div>
813
+ </div>
814
+ </div>
815
+ <template #footer>
816
+ <el-button @click="createGroupDialogVisible = false">取消</el-button>
817
+ <el-button type="primary" @click="handleCreateGroup" :disabled="!newGroupName.trim()">创建</el-button>
818
+ </template>
819
+ </el-dialog>
820
+
821
+ <!-- 群聊详情弹窗 -->
822
+ <el-dialog
823
+ v-if="currentSelectGroup"
824
+ v-model="groupDetailVisible"
825
+ :title="currentSelectGroup.name"
826
+ width="500px"
827
+ append-to-body
828
+ >
829
+ <div class="group-detail-content">
830
+ <div class="group-info-section">
831
+ <div class="group-info-item">
832
+ <span class="info-label">群ID</span>
833
+ <span class="info-value">{{ currentSelectGroup.groupId }}</span>
834
+ </div>
835
+ <div class="group-info-item">
836
+ <span class="info-label">群主</span>
837
+ <span class="info-value">{{ currentSelectGroup.owner }}</span>
838
+ </div>
839
+ <div class="group-info-item">
840
+ <span class="info-label">成员数</span>
841
+ <span class="info-value">{{ groupMemberList.length }}</span>
842
+ </div>
843
+ </div>
844
+
845
+ <div class="group-actions-section" v-if="currentSelectGroup.owner === myUsername">
846
+ <div class="section-header">
847
+ <span class="section-title">群管理</span>
848
+ </div>
849
+ <div class="group-actions-list">
850
+ <el-button type="primary" size="small" @click="openEditGroupInfo">
851
+ <el-icon><Edit /></el-icon>编辑群信息
852
+ </el-button>
853
+ <el-button type="danger" size="small" @click="handleDeleteGroup">
854
+ <el-icon><Delete /></el-icon>解散群聊
855
+ </el-button>
856
+ </div>
857
+ </div>
858
+
859
+ <div class="group-members-section">
860
+ <div class="section-header">
861
+ <span class="section-title">群成员</span>
862
+ <el-button size="small" @click="inviteMemberDialogVisible = true">
863
+ <el-icon><Plus /></el-icon>邀请成员
864
+ </el-button>
865
+ </div>
866
+ <div class="group-members-list">
867
+ <el-empty v-if="groupMemberList.length === 0" description="暂无成员" />
868
+ <div
869
+ v-else
870
+ v-for="member in groupMemberList"
871
+ :key="member.id || member.username"
872
+ class="group-member-item"
873
+ >
874
+ <img
875
+ :src="member.avatar ? `${props.config.api.baseUrl}${member.avatar}` : `https://api.dicebear.com/7.x/avataaars/svg?seed=${member.username}`"
876
+ :alt="member.username"
877
+ class="group-member-avatar"
878
+ />
879
+ <div class="group-member-info">
880
+ <span class="group-member-name">{{ member.memberNick || member.username }}</span>
881
+ <span v-if="member.username === currentSelectGroup.owner" class="group-member-badge">群主</span>
882
+ </div>
883
+ <div class="group-member-actions" v-if="currentSelectGroup.owner === myUsername && member.username !== myUsername">
884
+ <el-button size="small" link @click="openEditMemberNick(member)">
885
+ 改昵称
886
+ </el-button>
887
+ <el-button size="small" link type="danger" @click="handleRemoveMember(member.username)">
888
+ 移除
889
+ </el-button>
890
+ <el-button size="small" link type="warning" @click="handleTransferOwner(member.username)">
891
+ 转让
892
+ </el-button>
893
+ </div>
894
+ <div class="group-member-actions" v-else-if="member.username === myUsername">
895
+ <el-button size="small" link @click="openEditMemberNick(member)">
896
+ 改昵称
897
+ </el-button>
898
+ </div>
899
+ </div>
900
+ </div>
901
+ </div>
902
+ </div>
903
+ <template #footer>
904
+ <el-button @click="groupDetailVisible = false">关闭</el-button>
905
+ <el-button type="danger" @click="handleQuitGroup">退出群聊</el-button>
906
+ </template>
907
+ </el-dialog>
908
+
909
+ <!-- 编辑群信息弹窗 -->
910
+ <el-dialog
911
+ v-if="currentSelectGroup"
912
+ v-model="groupInfoVisible"
913
+ title="编辑群信息"
914
+ width="500px"
915
+ append-to-body
916
+ >
917
+ <el-form label-width="80px">
918
+ <el-form-item label="群名称">
919
+ <el-input v-model="editingGroupInfo.groupNickname" placeholder="请输入群名称" />
920
+ </el-form-item>
921
+ <el-form-item label="群备注">
922
+ <el-input v-model="editingGroupInfo.remark" type="textarea" :rows="3" placeholder="请输入群备注" />
923
+ </el-form-item>
924
+ <el-form-item label="群公告">
925
+ <el-input v-model="editingGroupInfo.notice" type="textarea" :rows="3" placeholder="请输入群公告" />
926
+ </el-form-item>
927
+ </el-form>
928
+ <template #footer>
929
+ <el-button @click="groupInfoVisible = false">取消</el-button>
930
+ <el-button type="primary" @click="handleUpdateGroupInfo">保存</el-button>
931
+ </template>
932
+ </el-dialog>
933
+
934
+ <!-- 编辑群成员昵称弹窗 -->
935
+ <el-dialog
936
+ v-if="currentSelectGroup"
937
+ v-model="memberNickDialogVisible"
938
+ title="编辑群昵称"
939
+ width="400px"
940
+ append-to-body
941
+ >
942
+ <el-form label-width="80px">
943
+ <el-form-item label="成员">
944
+ <span>{{ editingMemberNick.targetUsername }}</span>
945
+ </el-form-item>
946
+ <el-form-item label="群昵称">
947
+ <el-input v-model="editingMemberNick.memberNick" placeholder="请输入群昵称" />
948
+ </el-form-item>
949
+ </el-form>
950
+ <template #footer>
951
+ <el-button @click="memberNickDialogVisible = false">取消</el-button>
952
+ <el-button type="primary" @click="handleUpdateMemberNick">保存</el-button>
953
+ </template>
954
+ </el-dialog>
955
+
956
+ <!-- 消息已读成员弹窗 -->
957
+ <el-dialog
958
+ v-if="currentSelectGroup"
959
+ v-model="msgReadUserDialogVisible"
960
+ title="消息已读状态"
961
+ width="500px"
962
+ append-to-body
963
+ >
964
+ <div class="msg-read-users-content">
965
+ <div class="read-users-section">
966
+ <div class="section-title">
967
+ <el-icon><Check /></el-icon>已读 ({{ currentMsgReadList.readUserList.length }})
968
+ </div>
969
+ <div class="users-list">
970
+ <el-empty v-if="currentMsgReadList.readUserList.length === 0" description="暂无已读成员" />
971
+ <div v-else class="users-tag-list">
972
+ <el-tag v-for="user in currentMsgReadList.readUserList" :key="user" class="user-tag">
973
+ {{ user }}
974
+ </el-tag>
975
+ </div>
976
+ </div>
977
+ </div>
978
+ <div class="unread-users-section">
979
+ <div class="section-title">
980
+ <el-icon><Clock /></el-icon>未读 ({{ currentMsgReadList.unreadUserList.length }})
981
+ </div>
982
+ <div class="users-list">
983
+ <el-empty v-if="currentMsgReadList.unreadUserList.length === 0" description="全部已读" />
984
+ <div v-else class="users-tag-list">
985
+ <el-tag v-for="user in currentMsgReadList.unreadUserList" :key="user" type="info" class="user-tag">
986
+ {{ user }}
987
+ </el-tag>
988
+ </div>
989
+ </div>
990
+ </div>
991
+ </div>
992
+ <template #footer>
993
+ <el-button @click="msgReadUserDialogVisible = false">关闭</el-button>
994
+ </template>
995
+ </el-dialog>
996
+
997
+ <!-- 邀请成员弹窗 -->
998
+ <el-dialog
999
+ v-if="currentSelectGroup"
1000
+ v-model="inviteMemberDialogVisible"
1001
+ title="邀请成员"
1002
+ width="500px"
1003
+ append-to-body
1004
+ >
1005
+ <div class="invite-member-content">
1006
+ <div class="invite-member-list">
1007
+ <el-empty v-if="filteredFriendList.length === 0" description="暂无好友可以邀请" />
1008
+ <div
1009
+ v-else
1010
+ class="member-list"
1011
+ >
1012
+ <div
1013
+ v-for="friend in filteredFriendList"
1014
+ :key="friend.id"
1015
+ :class="[
1016
+ 'member-item',
1017
+ selectedMembersForInvite.some(m => m.id === friend.id) ? 'member-selected' : ''
1018
+ ]"
1019
+ @click="toggleMemberForInvite(friend)"
1020
+ >
1021
+ <img
1022
+ :src="friend.avatar"
1023
+ :alt="friend.name"
1024
+ class="member-avatar"
1025
+ />
1026
+ <span class="member-name">{{ friend.name }}</span>
1027
+ <el-icon v-if="selectedMembersForInvite.some(m => m.id === friend.id)" class="member-check"><CircleCheck /></el-icon>
1028
+ </div>
1029
+ </div>
1030
+ </div>
1031
+ </div>
1032
+ <template #footer>
1033
+ <el-button @click="inviteMemberDialogVisible = false">取消</el-button>
1034
+ <el-button type="primary" @click="handleInviteMember" :disabled="selectedMembersForInvite.length === 0">
1035
+ 邀请 {{ selectedMembersForInvite.length }} 人
1036
+ </el-button>
1037
+ </template>
1038
+ </el-dialog>
1039
+
488
1040
  <!-- 用户设置弹窗 -->
489
1041
  <el-dialog
490
1042
  v-model="showSettingsDialog"
@@ -661,10 +1213,17 @@ import {
661
1213
  Bell,
662
1214
  Setting,
663
1215
  Document,
664
- Download
1216
+ Download,
1217
+ CircleCheck,
1218
+ Edit,
1219
+ Delete,
1220
+ Check,
1221
+ Clock,
1222
+ Close,
1223
+ ArrowRight
665
1224
  } from '@element-plus/icons-vue'
666
1225
  import { useChat } from '../composables/useChat.js'
667
- import { ElMessage } from 'element-plus'
1226
+ import { ElMessage, ElMessageBox } from 'element-plus'
668
1227
  import AvatarCrop from './AvatarCrop.vue'
669
1228
  import EmojiPicker from './EmojiPicker.vue'
670
1229
 
@@ -694,6 +1253,33 @@ const {
694
1253
  loadingAvailableUsers,
695
1254
  friendApplyList,
696
1255
  loadingFriendApply,
1256
+ // ========== 群聊相关 ==========
1257
+ activeTab,
1258
+ groupList,
1259
+ currentSelectGroup,
1260
+ groupMsgList,
1261
+ groupMemberList,
1262
+ createGroupDialogVisible,
1263
+ groupDetailVisible,
1264
+ newGroupName,
1265
+ newGroupRemark,
1266
+ selectedMembersForCreate,
1267
+ inviteMemberDialogVisible,
1268
+ selectedMembersForInvite,
1269
+ filteredGroupList,
1270
+ currentChatTarget,
1271
+ currentGroupMessages,
1272
+ currentAllMessages,
1273
+ groupInfoVisible,
1274
+ editingGroupInfo,
1275
+ editingMemberNick,
1276
+ memberNickDialogVisible,
1277
+ msgReadUserDialogVisible,
1278
+ currentMsgReadList,
1279
+ currentGroupInfo,
1280
+ editingFields,
1281
+ tempEditValues,
1282
+ // ========== 方法 ==========
697
1283
  formatTime,
698
1284
  formatLastTime,
699
1285
  scrollToBottom,
@@ -712,6 +1298,34 @@ const {
712
1298
  loadFriendApplyList,
713
1299
  agreeFriend,
714
1300
  updateMyAvatar,
1301
+ // ========== 群聊相关方法 ==========
1302
+ getGroupList,
1303
+ getGroupHistory,
1304
+ getGroupMembers,
1305
+ selectGroup,
1306
+ createGroup,
1307
+ inviteGroupMember,
1308
+ quitGroup,
1309
+ sendGroupMessage,
1310
+ sendGroupFile,
1311
+ sendGroupFilesAndText,
1312
+ switchTab,
1313
+ getGroupUnreadCount,
1314
+ getMsgReadUserList,
1315
+ updateGroupInfo,
1316
+ updateMemberNick,
1317
+ deleteGroup,
1318
+ removeGroupMember,
1319
+ transferGroupOwner,
1320
+ readSingleGroupMsg,
1321
+ readAllGroupMsg,
1322
+ openEditGroupInfo,
1323
+ openEditMemberNick,
1324
+ fetchGroupDetail,
1325
+ updateSingleGroupField,
1326
+ startEditField,
1327
+ cancelEditField,
1328
+ saveEditField
715
1329
 
716
1330
  } = useChat(props.config, (message) => {
717
1331
  emit('message', message)
@@ -724,6 +1338,10 @@ const navTabs = computed(() => {
724
1338
  tabs.push({ id: 'friends', icon: UserFilled, badge: 0 })
725
1339
  }
726
1340
 
1341
+ if (props.config.modules.groups) {
1342
+ tabs.push({ id: 'groups', icon: UserFilled, badge: 0 })
1343
+ }
1344
+
727
1345
  if (props.config.modules.apply) {
728
1346
  tabs.push({ id: 'apply', icon: Bell, badge: friendApplyList.value?.length || 0 })
729
1347
  }
@@ -749,6 +1367,32 @@ const pendingFiles = ref([])
749
1367
  const contextMenu = ref({ visible: false, x: 0, y: 0, chat: null })
750
1368
  const showEmojiPicker = ref(false)
751
1369
 
1370
+ // 群聊侧边栏相关
1371
+ const showGroupSidebar = ref(false)
1372
+ const muteGroup = ref(false)
1373
+ const groupNameInputRef = ref(null)
1374
+
1375
+ // 监听侧边栏打开,获取群详情
1376
+ watch(showGroupSidebar, (newVal) => {
1377
+ if (newVal && currentSelectGroup.value) {
1378
+ fetchGroupDetail(currentSelectGroup.value.groupId)
1379
+ }
1380
+ })
1381
+
1382
+ // 计算属性:获取我在群里的昵称
1383
+ const myNicknameInGroup = computed(() => {
1384
+ const me = groupMemberList.value.find(m => m.username === myUsername)
1385
+ return me?.memberNick || ''
1386
+ })
1387
+
1388
+ // 打开编辑我自己的群昵称
1389
+ const openMyNicknameEdit = () => {
1390
+ const me = groupMemberList.value.find(m => m.username === myUsername)
1391
+ if (me) {
1392
+ openEditMemberNick(me)
1393
+ }
1394
+ }
1395
+
752
1396
  const showContextMenuFn = (e, chat) => {
753
1397
  e.preventDefault()
754
1398
  e.stopPropagation()
@@ -775,7 +1419,9 @@ const selectChat = (chat) => {
775
1419
  currentChatId.value = chat.id
776
1420
  currentChat.value = chat
777
1421
  currentSelectedFriend.value = null
1422
+ currentSelectGroup.value = null
778
1423
  showChatDetail.value = false
1424
+ groupDetailVisible.value = false
779
1425
  selectUser({
780
1426
  id: chat.id,
781
1427
  name: chat.name,
@@ -788,6 +1434,17 @@ const selectFriend = (friend) => {
788
1434
  currentSelectedFriend.value = friend
789
1435
  currentChatId.value = null
790
1436
  currentChat.value = null
1437
+ currentSelectGroup.value = null
1438
+ }
1439
+
1440
+ const selectGroupChat = (group) => {
1441
+ currentSelectGroup.value = group
1442
+ currentChatId.value = null
1443
+ currentChat.value = null
1444
+ currentSelectedFriend.value = null
1445
+ showChatDetail.value = false
1446
+ groupDetailVisible.value = false
1447
+ selectGroup(group)
791
1448
  }
792
1449
 
793
1450
  const handleStartChat = async () => {
@@ -874,7 +1531,15 @@ const handleSend = async () => {
874
1531
  })
875
1532
  pendingFiles.value = []
876
1533
 
877
- await sendFilesAndText(filesToSend, textToSend)
1534
+ // 根据当前选择的是群聊还是单聊来发送
1535
+ if (currentSelectGroup.value) {
1536
+ // 群聊发送
1537
+ await sendGroupFilesAndText(filesToSend, textToSend)
1538
+ } else {
1539
+ // 单聊发送
1540
+ await sendFilesAndText(filesToSend, textToSend)
1541
+ }
1542
+
878
1543
  emit('send', { text: textToSend, files: filesToSend })
879
1544
  }
880
1545
 
@@ -923,35 +1588,267 @@ const selectEmoji = (emoji) => {
923
1588
  showEmojiPicker.value = false
924
1589
  }
925
1590
 
926
- const handleAvatarFileChange = (e) => {
927
- const file = e.target.files[0]
928
- if (!file) return
1591
+ // 获取群聊头像网格样式
1592
+ const getGroupAvatarGridStyle = (count, index) => {
1593
+ const styles = {}
1594
+
1595
+ if (count === 1) {
1596
+ styles.width = '100%'
1597
+ styles.height = '100%'
1598
+ } else if (count === 2) {
1599
+ styles.width = '50%'
1600
+ styles.height = '100%'
1601
+ } else if (count === 3) {
1602
+ if (index === 0) {
1603
+ styles.width = '100%'
1604
+ styles.height = '50%'
1605
+ } else {
1606
+ styles.width = '50%'
1607
+ styles.height = '50%'
1608
+ }
1609
+ } else {
1610
+ styles.width = '50%'
1611
+ styles.height = '50%'
1612
+ }
1613
+
1614
+ return styles
1615
+ }
929
1616
 
930
- if (!file.type.startsWith('image/')) {
931
- ElMessage.error('只能上传图片文件')
932
- return
1617
+ // ========== 群聊相关方法 ==========
1618
+ const toggleMemberForCreate = (friend) => {
1619
+ const index = selectedMembersForCreate.value.findIndex(m => m.id === friend.id)
1620
+ if (index !== -1) {
1621
+ selectedMembersForCreate.value.splice(index, 1)
1622
+ } else {
1623
+ selectedMembersForCreate.value.push(friend)
933
1624
  }
1625
+ }
934
1626
 
935
- if (file.size > 5 * 1024 * 1024) {
936
- ElMessage.error('图片大小不能超过 5MB')
937
- return
1627
+ const toggleMemberForInvite = (friend) => {
1628
+ const index = selectedMembersForInvite.value.findIndex(m => m.id === friend.id)
1629
+ if (index !== -1) {
1630
+ selectedMembersForInvite.value.splice(index, 1)
1631
+ } else {
1632
+ selectedMembersForInvite.value.push(friend)
938
1633
  }
1634
+ }
939
1635
 
940
- const reader = new FileReader()
941
- reader.onload = (event) => {
942
- avatarImageSrc.value = event.target.result
943
- showAvatarEditor.value = true
1636
+ const handleCreateGroup = async () => {
1637
+ const success = await createGroup()
1638
+ if (success) {
1639
+ ElMessage.success('群聊创建成功')
1640
+ } else {
1641
+ ElMessage.error('群聊创建失败')
944
1642
  }
945
- reader.readAsDataURL(file)
946
1643
  }
947
1644
 
948
- const handleAvatarCropConfirm = async ({ file }) => {
949
- if (!file) return
1645
+ const handleInviteMember = async () => {
1646
+ const success = await inviteGroupMember()
1647
+ if (success) {
1648
+ ElMessage.success('邀请成功')
1649
+ } else {
1650
+ ElMessage.error('邀请失败')
1651
+ }
1652
+ }
950
1653
 
951
- avatarUploading.value = true
952
- try {
953
- const { ChatApi } = await import('../core/api.js')
954
- const api = new ChatApi(props.config)
1654
+ const handleQuitGroup = async () => {
1655
+ const confirm = await ElMessageBox.confirm('确定要退出群聊吗?', '提示', {
1656
+ confirmButtonText: '确定',
1657
+ cancelButtonText: '取消',
1658
+ type: 'warning'
1659
+ }).catch(() => {})
1660
+
1661
+ if (confirm) {
1662
+ const success = await quitGroup()
1663
+ if (success) {
1664
+ ElMessage.success('已退出群聊')
1665
+ } else {
1666
+ ElMessage.error('退出失败')
1667
+ }
1668
+ }
1669
+ }
1670
+
1671
+ // 处理解散群聊
1672
+ const handleDeleteGroup = async () => {
1673
+ const confirm = await ElMessageBox.confirm(
1674
+ '确定要解散该群聊吗?此操作不可恢复!',
1675
+ '警告',
1676
+ {
1677
+ confirmButtonText: '确定',
1678
+ cancelButtonText: '取消',
1679
+ type: 'warning'
1680
+ }
1681
+ ).catch(() => {})
1682
+
1683
+ if (confirm) {
1684
+ const success = await deleteGroup()
1685
+ if (success) {
1686
+ ElMessage.success('群聊已解散')
1687
+ showGroupSidebar.value = false
1688
+ } else {
1689
+ ElMessage.error('解散失败')
1690
+ }
1691
+ }
1692
+ }
1693
+
1694
+ // 处理保存编辑字段
1695
+ const handleSaveEditField = async (field) => {
1696
+ const loading = ElLoading.service({
1697
+ lock: true,
1698
+ text: '保存中...',
1699
+ background: 'rgba(0, 0, 0, 0.7)',
1700
+ })
1701
+
1702
+ try {
1703
+ const success = await saveEditField(field)
1704
+ if (success) {
1705
+ ElMessage.success('保存成功')
1706
+ } else {
1707
+ ElMessage.error('保存失败')
1708
+ }
1709
+ } catch (error) {
1710
+ console.error('保存失败', error)
1711
+ ElMessage.error('保存失败')
1712
+ } finally {
1713
+ loading.close()
1714
+ }
1715
+ }
1716
+
1717
+ // 处理保存群成员昵称
1718
+ const handleUpdateMemberNick = async () => {
1719
+ const loading = ElLoading.service({
1720
+ lock: true,
1721
+ text: '保存中...',
1722
+ background: 'rgba(0, 0, 0, 0.7)',
1723
+ })
1724
+
1725
+ try {
1726
+ const success = await updateMemberNick()
1727
+ if (success) {
1728
+ ElMessage.success('群昵称修改成功')
1729
+ memberNickDialogVisible.value = false
1730
+ } else {
1731
+ ElMessage.error('修改失败')
1732
+ }
1733
+ } catch (error) {
1734
+ console.error('修改失败', error)
1735
+ ElMessage.error('修改失败')
1736
+ } finally {
1737
+ loading.close()
1738
+ }
1739
+ }
1740
+
1741
+ // 处理移除群成员
1742
+ const handleRemoveMember = async (username) => {
1743
+ const confirm = await ElMessageBox.confirm(
1744
+ `确定要移除成员 ${username} 吗?`,
1745
+ '提示',
1746
+ {
1747
+ confirmButtonText: '确定',
1748
+ cancelButtonText: '取消',
1749
+ type: 'warning'
1750
+ }
1751
+ ).catch(() => {})
1752
+
1753
+ if (confirm) {
1754
+ const loading = ElLoading.service({
1755
+ lock: true,
1756
+ text: '移除中...',
1757
+ background: 'rgba(0, 0, 0, 0.7)',
1758
+ })
1759
+
1760
+ try {
1761
+ const success = await removeGroupMember(username)
1762
+ if (success) {
1763
+ ElMessage.success('成员已移除')
1764
+ } else {
1765
+ ElMessage.error('移除失败')
1766
+ }
1767
+ } catch (error) {
1768
+ console.error('移除失败', error)
1769
+ ElMessage.error('移除失败')
1770
+ } finally {
1771
+ loading.close()
1772
+ }
1773
+ }
1774
+ }
1775
+
1776
+ // 处理转让群主
1777
+ const handleTransferOwner = async (newOwnerUsername) => {
1778
+ const confirm = await ElMessageBox.confirm(
1779
+ `确定要将群主转让给 ${newOwnerUsername} 吗?`,
1780
+ '提示',
1781
+ {
1782
+ confirmButtonText: '确定',
1783
+ cancelButtonText: '取消',
1784
+ type: 'warning'
1785
+ }
1786
+ ).catch(() => {})
1787
+
1788
+ if (confirm) {
1789
+ const loading = ElLoading.service({
1790
+ lock: true,
1791
+ text: '转让中...',
1792
+ background: 'rgba(0, 0, 0, 0.7)',
1793
+ })
1794
+
1795
+ try {
1796
+ const success = await transferGroupOwner(newOwnerUsername)
1797
+ if (success) {
1798
+ ElMessage.success('群主已转让')
1799
+ } else {
1800
+ ElMessage.error('转让失败')
1801
+ }
1802
+ } catch (error) {
1803
+ console.error('转让失败', error)
1804
+ ElMessage.error('转让失败')
1805
+ } finally {
1806
+ loading.close()
1807
+ }
1808
+ }
1809
+ }
1810
+
1811
+ // 处理更新群信息
1812
+ const handleUpdateGroupInfo = async () => {
1813
+ const success = await updateGroupInfo()
1814
+ if (success) {
1815
+ ElMessage.success('群信息已更新')
1816
+ } else {
1817
+ ElMessage.error('更新失败')
1818
+ }
1819
+ }
1820
+
1821
+
1822
+
1823
+ const handleAvatarFileChange = (e) => {
1824
+ const file = e.target.files[0]
1825
+ if (!file) return
1826
+
1827
+ if (!file.type.startsWith('image/')) {
1828
+ ElMessage.error('只能上传图片文件')
1829
+ return
1830
+ }
1831
+
1832
+ if (file.size > 5 * 1024 * 1024) {
1833
+ ElMessage.error('图片大小不能超过 5MB')
1834
+ return
1835
+ }
1836
+
1837
+ const reader = new FileReader()
1838
+ reader.onload = (event) => {
1839
+ avatarImageSrc.value = event.target.result
1840
+ showAvatarEditor.value = true
1841
+ }
1842
+ reader.readAsDataURL(file)
1843
+ }
1844
+
1845
+ const handleAvatarCropConfirm = async ({ file }) => {
1846
+ if (!file) return
1847
+
1848
+ avatarUploading.value = true
1849
+ try {
1850
+ const { ChatApi } = await import('../core/api.js')
1851
+ const api = new ChatApi(props.config)
955
1852
  const res = await api.uploadAvatar(file, myUsername)
956
1853
  if (res.code === 200) {
957
1854
  ElMessage.success('头像上传成功')
@@ -1011,11 +1908,21 @@ const saveUserInfo = async () => {
1011
1908
 
1012
1909
  // 生命周期
1013
1910
  onMounted(async () => {
1014
- await Promise.all([getFriendList(), loadFriendApplyList()])
1911
+ const promises = [getFriendList(), loadFriendApplyList()]
1912
+
1913
+ // 如果启用了群聊模块,也加载群聊列表
1914
+ if (props.config.modules.groups) {
1915
+ promises.push(getGroupList())
1916
+ }
1917
+
1918
+ await Promise.all(promises)
1919
+
1015
1920
  initWebSocket()
1921
+
1016
1922
  if (filteredUsers.value.length > 0) {
1017
1923
  selectChat(filteredUsers.value[0])
1018
1924
  }
1925
+
1019
1926
  emit('init')
1020
1927
  document.addEventListener('click', hideContextMenu)
1021
1928
  })
@@ -1170,6 +2077,29 @@ onUnmounted(() => {
1170
2077
  object-fit: cover;
1171
2078
  }
1172
2079
 
2080
+ /* 多方格群聊头像 */
2081
+ .group-avatar-grid {
2082
+ width: 44px;
2083
+ height: 44px;
2084
+ border-radius: 8px;
2085
+ overflow: hidden;
2086
+ display: flex;
2087
+ flex-wrap: wrap;
2088
+ background-color: #f3f4f6;
2089
+ }
2090
+
2091
+ .group-avatar-item {
2092
+ overflow: hidden;
2093
+ box-sizing: border-box;
2094
+ border: 0.5px solid rgba(255, 255, 255, 0.3);
2095
+ }
2096
+
2097
+ .group-avatar-img {
2098
+ width: 100%;
2099
+ height: 100%;
2100
+ object-fit: cover;
2101
+ }
2102
+
1173
2103
  .chat-list-online-indicator {
1174
2104
  position: absolute;
1175
2105
  bottom: 0;
@@ -2137,4 +3067,542 @@ onUnmounted(() => {
2137
3067
  .chat-context-menu-item:hover {
2138
3068
  background-color: rgba(243, 244, 246, 0.8);
2139
3069
  }
3070
+
3071
+ /* ========== 群聊相关样式 ========== */
3072
+ .create-group-form {
3073
+ display: flex;
3074
+ flex-direction: column;
3075
+ gap: 20px;
3076
+ }
3077
+
3078
+ .form-item {
3079
+ display: flex;
3080
+ flex-direction: column;
3081
+ gap: 8px;
3082
+ }
3083
+
3084
+ .form-label {
3085
+ font-size: 14px;
3086
+ font-weight: 500;
3087
+ color: #374151;
3088
+ }
3089
+
3090
+ .member-selection {
3091
+ border: 1px solid #e5e7eb;
3092
+ border-radius: 8px;
3093
+ padding: 12px;
3094
+ background-color: #f9fafb;
3095
+ }
3096
+
3097
+ .member-list {
3098
+ display: grid;
3099
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
3100
+ gap: 12px;
3101
+ }
3102
+
3103
+ .member-item {
3104
+ display: flex;
3105
+ flex-direction: column;
3106
+ align-items: center;
3107
+ padding: 12px 8px;
3108
+ border-radius: 8px;
3109
+ cursor: pointer;
3110
+ transition: background-color 0.2s;
3111
+ border: 2px solid transparent;
3112
+ }
3113
+
3114
+ .member-item:hover {
3115
+ background-color: rgba(243, 244, 246, 0.8);
3116
+ }
3117
+
3118
+ .member-item.member-selected {
3119
+ border-color: #07c160;
3120
+ background-color: rgba(7, 193, 96, 0.05);
3121
+ }
3122
+
3123
+ .member-item img {
3124
+ width: 56px;
3125
+ height: 56px;
3126
+ border-radius: 50%;
3127
+ object-fit: cover;
3128
+ margin-bottom: 8px;
3129
+ }
3130
+
3131
+ .member-item span {
3132
+ font-size: 13px;
3133
+ color: #374151;
3134
+ text-align: center;
3135
+ overflow: hidden;
3136
+ text-overflow: ellipsis;
3137
+ white-space: nowrap;
3138
+ max-width: 100%;
3139
+ }
3140
+
3141
+ .member-check {
3142
+ color: #07c160;
3143
+ margin-top: 4px;
3144
+ }
3145
+
3146
+ .group-detail-content {
3147
+ display: flex;
3148
+ flex-direction: column;
3149
+ gap: 16px;
3150
+ }
3151
+
3152
+ .group-info-section {
3153
+ display: flex;
3154
+ flex-direction: column;
3155
+ gap: 12px;
3156
+ }
3157
+
3158
+ .group-info-item {
3159
+ display: flex;
3160
+ justify-content: space-between;
3161
+ align-items: center;
3162
+ padding: 8px 0;
3163
+ }
3164
+
3165
+ .info-label {
3166
+ font-size: 14px;
3167
+ color: #6b7280;
3168
+ }
3169
+
3170
+ .info-value {
3171
+ font-size: 14px;
3172
+ color: #1f2937;
3173
+ font-weight: 500;
3174
+ }
3175
+
3176
+ .group-members-section {
3177
+ display: flex;
3178
+ flex-direction: column;
3179
+ gap: 12px;
3180
+ }
3181
+
3182
+ .section-header {
3183
+ display: flex;
3184
+ justify-content: space-between;
3185
+ align-items: center;
3186
+ }
3187
+
3188
+ .section-title {
3189
+ font-size: 14px;
3190
+ font-weight: 500;
3191
+ color: #374151;
3192
+ }
3193
+
3194
+ .group-members-list {
3195
+ display: flex;
3196
+ flex-direction: column;
3197
+ gap: 8px;
3198
+ max-height: 300px;
3199
+ overflow-y: auto;
3200
+ }
3201
+
3202
+ .group-member-item {
3203
+ display: flex;
3204
+ align-items: center;
3205
+ gap: 12px;
3206
+ padding: 8px 12px;
3207
+ border-radius: 8px;
3208
+ background-color: white;
3209
+ transition: background-color 0.2s;
3210
+ }
3211
+
3212
+ .group-member-item:hover {
3213
+ background-color: #f9fafb;
3214
+ }
3215
+
3216
+ .group-member-avatar {
3217
+ width: 40px;
3218
+ height: 40px;
3219
+ border-radius: 50%;
3220
+ object-fit: cover;
3221
+ flex-shrink: 0;
3222
+ }
3223
+
3224
+ .group-member-name {
3225
+ font-size: 14px;
3226
+ color: #1f2937;
3227
+ flex: 1;
3228
+ overflow: hidden;
3229
+ text-overflow: ellipsis;
3230
+ white-space: nowrap;
3231
+ }
3232
+
3233
+ /* ========== 群聊侧边栏样式 ========== */
3234
+ .group-sidebar {
3235
+ width: 320px;
3236
+ background-color: #f5f5f5;
3237
+ border-left: 1px solid #e5e7eb;
3238
+ display: flex;
3239
+ flex-direction: column;
3240
+ height: 100%;
3241
+ overflow: hidden;
3242
+ }
3243
+
3244
+ .slide-enter-active,
3245
+ .slide-leave-active {
3246
+ transition: transform 0.3s ease;
3247
+ }
3248
+
3249
+ .slide-enter-from,
3250
+ .slide-leave-to {
3251
+ transform: translateX(100%);
3252
+ }
3253
+
3254
+ .group-sidebar-header {
3255
+ height: 56px;
3256
+ display: flex;
3257
+ justify-content: space-between;
3258
+ align-items: center;
3259
+ padding: 0 16px;
3260
+ border-bottom: 1px solid #e5e7eb;
3261
+ background-color: white;
3262
+ }
3263
+
3264
+ .group-sidebar-title {
3265
+ font-size: 16px;
3266
+ font-weight: 500;
3267
+ color: #1f2937;
3268
+ }
3269
+
3270
+ .group-sidebar-close {
3271
+ width: 32px;
3272
+ height: 32px;
3273
+ display: flex;
3274
+ align-items: center;
3275
+ justify-content: center;
3276
+ border-radius: 50%;
3277
+ cursor: pointer;
3278
+ transition: background-color 0.2s;
3279
+ color: #6b7280;
3280
+ }
3281
+
3282
+ .group-sidebar-close:hover {
3283
+ background-color: #f3f4f6;
3284
+ color: #374151;
3285
+ }
3286
+
3287
+ .group-sidebar-content {
3288
+ flex: 1;
3289
+ overflow-y: auto;
3290
+ padding: 16px;
3291
+ }
3292
+
3293
+ .group-members-avatar-section {
3294
+ margin-bottom: 16px;
3295
+ }
3296
+
3297
+ .group-members-grid {
3298
+ display: grid;
3299
+ grid-template-columns: repeat(4, 1fr);
3300
+ gap: 16px;
3301
+ }
3302
+
3303
+ .group-member-avatar-item {
3304
+ display: flex;
3305
+ flex-direction: column;
3306
+ align-items: center;
3307
+ cursor: pointer;
3308
+ padding: 4px;
3309
+ border-radius: 8px;
3310
+ transition: background-color 0.2s;
3311
+ }
3312
+
3313
+ .group-member-avatar-item:hover {
3314
+ background-color: rgba(0, 0, 0, 0.05);
3315
+ }
3316
+
3317
+ .group-member-avatar-small {
3318
+ width: 48px;
3319
+ height: 48px;
3320
+ border-radius: 50%;
3321
+ object-fit: cover;
3322
+ margin-bottom: 4px;
3323
+ }
3324
+
3325
+ .group-member-add-icon {
3326
+ width: 48px;
3327
+ height: 48px;
3328
+ border-radius: 50%;
3329
+ background-color: #f3f4f6;
3330
+ display: flex;
3331
+ align-items: center;
3332
+ justify-content: center;
3333
+ color: #6b7280;
3334
+ margin-bottom: 4px;
3335
+ }
3336
+
3337
+ .group-member-nickname {
3338
+ font-size: 12px;
3339
+ color: #6b7280;
3340
+ text-align: center;
3341
+ overflow: hidden;
3342
+ text-overflow: ellipsis;
3343
+ white-space: nowrap;
3344
+ max-width: 100%;
3345
+ }
3346
+
3347
+ .group-divider {
3348
+ height: 12px;
3349
+ background-color: transparent;
3350
+ border-top: 1px solid #e5e7eb;
3351
+ margin: 16px 0;
3352
+ }
3353
+
3354
+ .group-settings-section {
3355
+ display: flex;
3356
+ flex-direction: column;
3357
+ gap: 4px;
3358
+ }
3359
+
3360
+ .group-setting-item {
3361
+ display: flex;
3362
+ justify-content: space-between;
3363
+ align-items: center;
3364
+ padding: 14px 16px;
3365
+ background-color: white;
3366
+ cursor: pointer;
3367
+ transition: background-color 0.2s;
3368
+ border-radius: 8px;
3369
+ margin-bottom: 4px;
3370
+ }
3371
+
3372
+ .group-setting-item:hover {
3373
+ background-color: #f9fafb;
3374
+ }
3375
+
3376
+ .group-setting-item.danger {
3377
+ color: #ef4444;
3378
+ margin-top: 8px;
3379
+ }
3380
+
3381
+ .group-setting-item.danger:hover {
3382
+ background-color: #fef2f2;
3383
+ }
3384
+
3385
+ .group-setting-label {
3386
+ font-size: 14px;
3387
+ color: #1f2937;
3388
+ }
3389
+
3390
+ .group-setting-item.danger .group-setting-label {
3391
+ color: #ef4444;
3392
+ }
3393
+
3394
+ .group-setting-value {
3395
+ display: flex;
3396
+ align-items: center;
3397
+ gap: 8px;
3398
+ color: #6b7280;
3399
+ font-size: 14px;
3400
+ }
3401
+
3402
+ .group-setting-value-text {
3403
+ max-width: 160px;
3404
+ overflow: hidden;
3405
+ text-overflow: ellipsis;
3406
+ white-space: nowrap;
3407
+ }
3408
+
3409
+ .group-setting-value-wrapper {
3410
+ display: flex;
3411
+ flex: 1;
3412
+ justify-content: flex-end;
3413
+ }
3414
+
3415
+ .group-edit-wrapper {
3416
+ width: 100%;
3417
+ display: flex;
3418
+ flex-direction: column;
3419
+ gap: 12px;
3420
+ }
3421
+
3422
+ .group-edit-buttons {
3423
+ display: flex;
3424
+ justify-content: flex-end;
3425
+ gap: 8px;
3426
+ }
3427
+
3428
+ .group-input-edit {
3429
+ flex: 1;
3430
+ }
3431
+
3432
+ .group-edit-actions {
3433
+ display: flex;
3434
+ gap: 8px;
3435
+ justify-content: flex-end;
3436
+ margin-top: 8px;
3437
+ }
3438
+
3439
+ /* ========== 消息已读成员弹窗样式 ========== */
3440
+ .msg-read-users-content {
3441
+ display: flex;
3442
+ flex-direction: column;
3443
+ gap: 20px;
3444
+ }
3445
+
3446
+ .read-users-section,
3447
+ .unread-users-section {
3448
+ display: flex;
3449
+ flex-direction: column;
3450
+ gap: 12px;
3451
+ }
3452
+
3453
+ .users-list {
3454
+ display: flex;
3455
+ flex-direction: column;
3456
+ gap: 8px;
3457
+ }
3458
+
3459
+ .users-tag-list {
3460
+ display: flex;
3461
+ flex-wrap: wrap;
3462
+ gap: 8px;
3463
+ }
3464
+
3465
+ .user-tag {
3466
+ padding: 6px 12px;
3467
+ background-color: #f3f4f6;
3468
+ border-radius: 9999px;
3469
+ font-size: 13px;
3470
+ color: #374151;
3471
+
3472
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
3473
+ gap: 12px;
3474
+ }
3475
+
3476
+ .member-item {
3477
+ display: flex;
3478
+ align-items: center;
3479
+ gap: 8px;
3480
+ padding: 8px 12px;
3481
+ border-radius: 8px;
3482
+ cursor: pointer;
3483
+ transition: all 0.2s;
3484
+ border: 2px solid transparent;
3485
+ background-color: white;
3486
+ }
3487
+
3488
+ .member-item:hover {
3489
+ background-color: #f3f4f6;
3490
+ }
3491
+
3492
+ .member-selected {
3493
+ background-color: rgba(7, 193, 96, 0.1);
3494
+ border-color: #07c160;
3495
+ }
3496
+
3497
+ .member-avatar {
3498
+ width: 40px;
3499
+ height: 40px;
3500
+ border-radius: 50%;
3501
+ object-fit: cover;
3502
+ }
3503
+
3504
+ .member-name {
3505
+ flex: 1;
3506
+ font-size: 14px;
3507
+ color: #374151;
3508
+ overflow: hidden;
3509
+ text-overflow: ellipsis;
3510
+ white-space: nowrap;
3511
+ }
3512
+
3513
+ .member-check {
3514
+ color: #07c160;
3515
+ font-size: 20px;
3516
+ }
3517
+
3518
+ .group-detail-content {
3519
+ display: flex;
3520
+ flex-direction: column;
3521
+ gap: 20px;
3522
+ }
3523
+
3524
+ .group-info-section {
3525
+ display: flex;
3526
+ flex-direction: column;
3527
+ gap: 12px;
3528
+ }
3529
+
3530
+ .group-info-item {
3531
+ display: flex;
3532
+ justify-content: space-between;
3533
+ align-items: center;
3534
+ padding: 12px;
3535
+ background-color: #f9fafb;
3536
+ border-radius: 8px;
3537
+ }
3538
+
3539
+ .info-label {
3540
+ font-size: 14px;
3541
+ color: #6b7280;
3542
+ }
3543
+
3544
+ .info-value {
3545
+ font-size: 14px;
3546
+ color: #374151;
3547
+ font-weight: 500;
3548
+ }
3549
+
3550
+ .group-members-section {
3551
+ display: flex;
3552
+ flex-direction: column;
3553
+ gap: 12px;
3554
+ }
3555
+
3556
+ .section-header {
3557
+ display: flex;
3558
+ justify-content: space-between;
3559
+ align-items: center;
3560
+ }
3561
+
3562
+ .section-title {
3563
+ font-size: 14px;
3564
+ font-weight: 500;
3565
+ color: #374151;
3566
+ }
3567
+
3568
+ .group-members-list {
3569
+ display: grid;
3570
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
3571
+ gap: 12px;
3572
+ max-height: 300px;
3573
+ overflow-y: auto;
3574
+ }
3575
+
3576
+ .group-member-item {
3577
+ display: flex;
3578
+ flex-direction: column;
3579
+ align-items: center;
3580
+ gap: 8px;
3581
+ padding: 12px;
3582
+ border-radius: 8px;
3583
+ background-color: #f9fafb;
3584
+ text-align: center;
3585
+ }
3586
+
3587
+ .group-member-avatar {
3588
+ width: 50px;
3589
+ height: 50px;
3590
+ border-radius: 50%;
3591
+ object-fit: cover;
3592
+ }
3593
+
3594
+ .group-member-name {
3595
+ font-size: 12px;
3596
+ color: #374151;
3597
+ overflow: hidden;
3598
+ text-overflow: ellipsis;
3599
+ white-space: nowrap;
3600
+ width: 100%;
3601
+ }
3602
+
3603
+ .invite-member-content {
3604
+ display: flex;
3605
+ flex-direction: column;
3606
+ gap: 16px;
3607
+ }
2140
3608
  </style>