vue-chat-kit 0.1.3 → 0.2.1

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.
@@ -0,0 +1,2089 @@
1
+ <template>
2
+ <div class="chat-panel">
3
+ <!-- 左侧图标导航栏 -->
4
+ <div class="chat-sidebar">
5
+ <div class="sidebar-avatar" @click="handleAvatarClick">
6
+ <img :src="myAvatar" alt="头像" class="sidebar-avatar-img" />
7
+ </div>
8
+
9
+ <div
10
+ v-for="tab in navTabs"
11
+ :key="tab.id"
12
+ :class="[
13
+ 'sidebar-nav-item',
14
+ currentNavTab === tab.id ? 'sidebar-nav-item-active' : 'sidebar-nav-item-inactive'
15
+ ]"
16
+ @click="currentNavTab = tab.id"
17
+ >
18
+ <el-icon :size="24">
19
+ <component :is="tab.icon" />
20
+ </el-icon>
21
+ <span v-if="tab.badge" class="sidebar-nav-badge">
22
+ {{ tab.badge > 99 ? '99+' : tab.badge }}
23
+ </span>
24
+ </div>
25
+
26
+ <div class="sidebar-spacer"></div>
27
+
28
+ <div
29
+ v-if="config.modules.settings"
30
+ class="sidebar-nav-item sidebar-nav-item-inactive"
31
+ @click="showSettingsDialog = true"
32
+ title="设置"
33
+ >
34
+ <el-icon :size="24"><Setting /></el-icon>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- 中间内容栏 -->
39
+ <div class="chat-content-panel">
40
+ <!-- 搜索栏 -->
41
+ <div class="chat-search-bar">
42
+ <el-input
43
+ v-model="searchText"
44
+ placeholder="搜索"
45
+ :prefix-icon="Search"
46
+ class="chat-search-input"
47
+ />
48
+ </div>
49
+
50
+ <!-- 内容区域 -->
51
+ <div class="chat-content-scroll">
52
+ <!-- 聊天列表 -->
53
+ <div v-if="currentNavTab === 'chat'">
54
+ <div
55
+ v-for="chat in filteredUsers"
56
+ :key="chat.id"
57
+ :class="[
58
+ 'chat-list-item',
59
+ currentChatId === chat.id ? 'chat-list-item-active' : ''
60
+ ]"
61
+ @click="selectChat(chat)"
62
+ @contextmenu.prevent.stop="showContextMenuFn($event, chat)"
63
+ >
64
+ <div class="chat-list-avatar-wrapper">
65
+ <img
66
+ :src="chat.avatar"
67
+ :alt="chat.name"
68
+ class="chat-list-avatar"
69
+ />
70
+ <span
71
+ v-if="chat.online"
72
+ class="chat-list-online-indicator"
73
+ ></span>
74
+ </div>
75
+ <div class="chat-list-info">
76
+ <div class="chat-list-header">
77
+ <span class="chat-list-name">{{ chat.name }}</span>
78
+ <span class="chat-list-time">{{ formatLastTime(chat.lastTime) }}</span>
79
+ </div>
80
+ <div class="chat-list-preview">
81
+ <span class="chat-list-last-msg">{{ chat.lastMsg }}</span>
82
+ <span
83
+ v-if="chat.unread > 0"
84
+ class="chat-list-unread"
85
+ >
86
+ {{ chat.unread > 99 ? '99+' : chat.unread }}
87
+ </span>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- 好友列表 -->
94
+ <div v-if="currentNavTab === 'friends' && config.modules.friends">
95
+ <div class="add-friend-section">
96
+ <div
97
+ class="add-friend-btn"
98
+ @click="openAddFriendDialog"
99
+ >
100
+ <div class="add-friend-icon">
101
+ <el-icon class="text-white" :size="20"><Plus /></el-icon>
102
+ </div>
103
+ <span class="add-friend-text">添加好友</span>
104
+ </div>
105
+ </div>
106
+ <div
107
+ v-for="friend in filteredFriendList"
108
+ :key="friend.id"
109
+ class="chat-list-item"
110
+ @click="selectFriend(friend)"
111
+ >
112
+ <div class="chat-list-avatar-wrapper">
113
+ <img
114
+ :src="friend.avatar"
115
+ :alt="friend.name"
116
+ class="chat-list-avatar"
117
+ />
118
+ <span
119
+ :class="[
120
+ 'chat-list-online-indicator',
121
+ friend.online ? 'chat-list-online' : 'chat-list-offline'
122
+ ]"
123
+ ></span>
124
+ </div>
125
+ <div class="chat-list-info">
126
+ <span class="chat-list-name">{{ friend.name }}</span>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <!-- 申请列表 -->
132
+ <div v-if="currentNavTab === 'apply' && config.modules.apply">
133
+ <el-empty v-if="loadingFriendApply" description="加载中..." />
134
+ <el-empty
135
+ v-else-if="friendApplyList.length === 0"
136
+ description="暂无好友申请"
137
+ />
138
+ <div
139
+ v-else
140
+ v-for="apply in friendApplyList"
141
+ :key="apply.applyUser || apply.id"
142
+ class="friend-request-item"
143
+ >
144
+ <div class="friend-request-info">
145
+ <img
146
+ :src="`https://api.dicebear.com/7.x/avataaars/svg?seed=${apply.applyUser}`"
147
+ :alt="apply.applyUser"
148
+ class="friend-request-avatar"
149
+ />
150
+ <div class="friend-request-details">
151
+ <div class="friend-request-username">{{ apply.applyUser }}</div>
152
+ <div class="friend-request-desc">请求添加你为好友</div>
153
+ </div>
154
+ </div>
155
+ <el-button
156
+ type="primary"
157
+ size="small"
158
+ @click="agreeFriend(apply.applyUser)"
159
+ >同意</el-button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- 右侧聊天/详情区域 -->
166
+ <div class="chat-main-area">
167
+ <!-- 好友信息展示区域 -->
168
+ <div v-if="currentSelectedFriend && !currentChat" class="friend-profile-area">
169
+ <img
170
+ :src="currentSelectedFriend.avatar"
171
+ :alt="currentSelectedFriend.name"
172
+ class="profile-avatar"
173
+ />
174
+ <div class="profile-name">{{ currentSelectedFriend.name }}</div>
175
+ <div class="profile-status">
176
+ <span
177
+ :class="[
178
+ 'profile-status-dot',
179
+ currentSelectedFriend.online ? 'profile-status-online' : 'profile-status-offline'
180
+ ]"
181
+ ></span>
182
+ <span>{{ currentSelectedFriend.online ? '在线' : '离线' }}</span>
183
+ </div>
184
+ <el-button
185
+ type="primary"
186
+ size="large"
187
+ @click="handleStartChat"
188
+ class="profile-start-chat-btn"
189
+ >
190
+ <el-icon><ChatDotRound /></el-icon>
191
+ <span>发消息</span>
192
+ </el-button>
193
+ </div>
194
+
195
+ <!-- 聊天窗口 -->
196
+ <div v-if="currentChat" class="chat-window-area">
197
+ <!-- 顶部标题栏 -->
198
+ <div class="chat-window-header">
199
+ <div class="chat-window-title">
200
+ <span class="chat-window-name">{{ currentChat.name }}</span>
201
+ <span
202
+ :class="[
203
+ 'chat-window-status',
204
+ currentChat.online ? 'chat-window-status-online' : 'chat-window-status-offline'
205
+ ]"
206
+ >
207
+ {{ currentChat.online ? '在线' : '离线' }}
208
+ </span>
209
+ </div>
210
+ <div class="chat-window-actions">
211
+ <el-icon class="chat-action-icon"><Search /></el-icon>
212
+ <el-icon
213
+ class="chat-action-icon"
214
+ @click="showChatDetail = !showChatDetail"
215
+ ><MoreFilled /></el-icon>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- 聊天消息区域 -->
220
+ <div
221
+ ref="messagesContainer"
222
+ class="chat-messages-container"
223
+ >
224
+ <div
225
+ v-for="(msg, index) in currentMessages"
226
+ :key="index"
227
+ :class="[
228
+ 'message-wrapper',
229
+ msg.isSelf ? 'message-self' : 'message-other'
230
+ ]"
231
+ >
232
+ <!-- 头像 -->
233
+ <div class="message-avatar">
234
+ <img
235
+ :src="msg.isSelf ? myAvatar : currentChat.avatar"
236
+ class="message-avatar-img"
237
+ />
238
+ </div>
239
+
240
+ <!-- 消息内容 -->
241
+ <div
242
+ :class="[
243
+ 'message-content',
244
+ msg.isSelf ? 'message-content-self' : 'message-content-other'
245
+ ]"
246
+ >
247
+ <div v-if="!msg.isSelf" class="message-sender-name">{{ currentChat.name }}</div>
248
+
249
+ <div class="message-bubble-wrapper">
250
+ <!-- 文本消息 -->
251
+ <div
252
+ v-if="msg.type === 'text'"
253
+ :class="[
254
+ 'message-bubble',
255
+ msg.isSelf ? 'message-bubble-self' : 'message-bubble-other'
256
+ ]"
257
+ >
258
+ {{ msg.text }}
259
+ </div>
260
+
261
+ <!-- 图片文件消息 -->
262
+ <div
263
+ v-else-if="msg.type === 'file' && msg.fileType === 'image'"
264
+ :class="[
265
+ 'message-bubble',
266
+ 'message-image-bubble',
267
+ msg.isSelf ? 'message-bubble-self' : 'message-bubble-other'
268
+ ]"
269
+ @click="openFile(msg.fileUrl)"
270
+ >
271
+ <img
272
+ :src="msg.fileUrl"
273
+ :alt="msg.fileName"
274
+ class="message-image"
275
+ @error="handleImageError"
276
+ />
277
+ <div v-if="msg.fileSize" class="message-image-size">{{ formatFileSize(msg.fileSize) }}</div>
278
+ </div>
279
+
280
+ <!-- 文档文件消息 -->
281
+ <div
282
+ v-else-if="msg.type === 'file'"
283
+ :class="[
284
+ 'message-bubble',
285
+ 'message-file-bubble',
286
+ msg.isSelf ? 'message-bubble-self' : 'message-bubble-other'
287
+ ]"
288
+ @click="openFile(msg.fileUrl)"
289
+ >
290
+ <div class="message-file-content">
291
+ <div class="message-file-icon">
292
+ <el-icon :size="28"><Document /></el-icon>
293
+ </div>
294
+ <div class="message-file-info">
295
+ <div class="message-file-name">{{ msg.fileName || msg.text }}</div>
296
+ <div class="message-file-meta">
297
+ <el-icon :size="12"><Download /></el-icon>
298
+ <span>点击下载</span>
299
+ <span v-if="msg.fileSize">· {{ formatFileSize(msg.fileSize) }}</span>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ <!-- 时间显示在气泡下方 -->
306
+ <div
307
+ :class="[
308
+ 'message-time',
309
+ msg.isSelf ? 'message-time-right' : 'message-time-left'
310
+ ]"
311
+ >
312
+ {{ formatTime(msg.time) }}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- 底部输入区域 -->
320
+ <div class="chat-input-area">
321
+ <!-- 待发送文件预览 -->
322
+ <div
323
+ v-if="pendingFiles.length > 0"
324
+ class="pending-files-area"
325
+ >
326
+ <div
327
+ v-for="(file, index) in pendingFiles"
328
+ :key="file.id"
329
+ class="pending-file-item"
330
+ >
331
+ <!-- 图片预览 -->
332
+ <div
333
+ v-if="file.isImage"
334
+ class="pending-image-wrapper"
335
+ >
336
+ <img
337
+ :src="file.previewUrl"
338
+ :alt="file.name"
339
+ class="pending-image"
340
+ />
341
+ <button
342
+ @click="removePendingFile(index)"
343
+ class="pending-file-remove-btn"
344
+ >
345
+ ×
346
+ </button>
347
+ </div>
348
+ <!-- 非图片文件预览 -->
349
+ <div
350
+ v-else
351
+ class="pending-file-wrapper"
352
+ >
353
+ <el-icon class="pending-file-icon"><Folder /></el-icon>
354
+ <span class="pending-file-name">{{ file.name }}</span>
355
+ <button
356
+ @click="removePendingFile(index)"
357
+ class="pending-file-remove-btn"
358
+ >
359
+ ×
360
+ </button>
361
+ </div>
362
+ </div>
363
+ </div>
364
+
365
+ <div v-if="config.modules.fileUpload" class="input-toolbar">
366
+ <el-icon class="input-toolbar-icon"><ChatDotRound /></el-icon>
367
+ <el-icon
368
+ class="input-toolbar-icon"
369
+ @click="triggerFileSelect"
370
+ ><Folder /></el-icon>
371
+ <el-icon class="input-toolbar-icon"><Picture /></el-icon>
372
+ </div>
373
+ <div class="input-textarea-wrapper">
374
+ <textarea
375
+ v-model="inputText"
376
+ @keydown.enter.prevent="handleSend"
377
+ @paste="handlePaste"
378
+ placeholder="输入消息或粘贴文件..."
379
+ class="message-input-textarea"
380
+ rows="3"
381
+ />
382
+ </div>
383
+ <div class="input-send-wrapper">
384
+ <el-button
385
+ type="primary"
386
+ :disabled="!inputText.trim() && pendingFiles.length === 0"
387
+ @click="handleSend"
388
+ class="send-message-btn"
389
+ >
390
+ 发送
391
+ </el-button>
392
+ </div>
393
+
394
+ <!-- 隐藏的文件 input -->
395
+ <input
396
+ ref="fileInputRef"
397
+ type="file"
398
+ multiple
399
+ class="hidden-file-input"
400
+ @change="handleFileSelect"
401
+ />
402
+ </div>
403
+ </div>
404
+
405
+ <!-- 空状态 -->
406
+ <div
407
+ v-else-if="!currentSelectedFriend"
408
+ class="chat-empty-state"
409
+ >
410
+ <el-icon :size="64" class="empty-state-icon"><ChatLineRound /></el-icon>
411
+ <div class="empty-state-text">
412
+ {{ currentNavTab === 'apply' ? '在左侧选择好友申请' : '在左侧选择好友开始聊天' }}
413
+ </div>
414
+ </div>
415
+ </div>
416
+
417
+ <!-- 右侧详情面板 -->
418
+ <div
419
+ v-if="showChatDetail"
420
+ class="chat-detail-panel"
421
+ >
422
+ <div class="chat-detail-header">聊天详情</div>
423
+ <div class="chat-detail-content">
424
+ <div class="chat-detail-profile">
425
+ <img
426
+ :src="currentChat?.avatar"
427
+ :alt="currentChat?.name"
428
+ class="chat-detail-avatar"
429
+ />
430
+ <div class="chat-detail-name">{{ currentChat?.name }}</div>
431
+ <div class="chat-detail-actions">
432
+ <div class="chat-detail-action-item">查找聊天记录</div>
433
+ <div class="chat-detail-action-item">清空聊天记录</div>
434
+ </div>
435
+ </div>
436
+ </div>
437
+ </div>
438
+
439
+ <!-- 添加好友弹窗 -->
440
+ <el-dialog
441
+ v-model="addFriendDialogVisible"
442
+ title="添加好友"
443
+ width="500px"
444
+ append-to-body
445
+ >
446
+ <div class="add-friend-search-wrapper">
447
+ <el-input
448
+ v-model="addFriendSearchText"
449
+ placeholder="搜索用户"
450
+ :prefix-icon="Search"
451
+ class="add-friend-search-input"
452
+ />
453
+ </div>
454
+ <div class="add-friend-users-list">
455
+ <el-empty v-if="loadingAvailableUsers" description="加载中..." />
456
+ <el-empty
457
+ v-else-if="filteredAvailableUsers.length === 0"
458
+ description="暂无用户"
459
+ />
460
+ <div
461
+ v-else
462
+ v-for="user in filteredAvailableUsers"
463
+ :key="user.username"
464
+ class="add-friend-user-item"
465
+ >
466
+ <div class="add-friend-user-info">
467
+ <img
468
+ :src="`https://api.dicebear.com/7.x/avataaars/svg?seed=${user.username}`"
469
+ :alt="user.username"
470
+ class="add-friend-user-avatar"
471
+ />
472
+ <div class="add-friend-user-name">{{ user.username }}</div>
473
+ </div>
474
+ <el-button type="primary" size="small" @click="addFriend(user)">添加</el-button>
475
+ </div>
476
+ </div>
477
+ </el-dialog>
478
+
479
+ <!-- 用户设置弹窗 -->
480
+ <el-dialog
481
+ v-model="showSettingsDialog"
482
+ title="个人设置"
483
+ width="560px"
484
+ :close-on-click-modal="false"
485
+ append-to-body
486
+ class="chat-settings-dialog"
487
+ >
488
+ <div class="chat-settings-container">
489
+ <!-- 头像设置区域 -->
490
+ <div class="chat-settings-avatar-section">
491
+ <div class="chat-settings-avatar-wrapper">
492
+ <img
493
+ :src="myAvatar"
494
+ alt="头像"
495
+ class="chat-settings-avatar"
496
+ />
497
+ <div
498
+ v-if="config.modules.avatarCrop"
499
+ class="chat-settings-avatar-edit"
500
+ @click="triggerAvatarUpload"
501
+ >
502
+ <el-icon :size="18" class="chat-settings-avatar-icon"><Camera /></el-icon>
503
+ </div>
504
+ <input
505
+ ref="avatarInputRef"
506
+ type="file"
507
+ accept="image/*"
508
+ class="hidden-avatar-input"
509
+ @change="handleAvatarFileChange"
510
+ />
511
+ </div>
512
+ <div class="chat-settings-user-display">
513
+ <div class="chat-settings-nickname">{{ userInfo.nickname || myUsername }}</div>
514
+ <div class="chat-settings-username">@{{ myUsername }}</div>
515
+ </div>
516
+ </div>
517
+
518
+ <!-- 用户信息表单 -->
519
+ <div class="chat-settings-form-section">
520
+ <div class="chat-settings-form-header">
521
+ <div class="chat-settings-form-title">
522
+ <el-icon><UserFilled /></el-icon>
523
+ 个人信息
524
+ </div>
525
+ <el-button
526
+ v-if="!isEditingUserInfo"
527
+ type="primary"
528
+ size="small"
529
+ @click="startEditUserInfo"
530
+ class="chat-settings-edit-btn"
531
+ >
532
+ 编辑
533
+ </el-button>
534
+ </div>
535
+
536
+ <div class="chat-settings-form">
537
+ <!-- 昵称 -->
538
+ <div class="chat-settings-form-item">
539
+ <label class="chat-settings-form-label">昵称</label>
540
+ <el-input
541
+ v-if="isEditingUserInfo"
542
+ v-model="editingUserInfo.nickname"
543
+ placeholder="请输入昵称"
544
+ size="large"
545
+ />
546
+ <div
547
+ v-else
548
+ class="chat-settings-form-value"
549
+ >
550
+ {{ userInfo.nickname || '未设置' }}
551
+ </div>
552
+ </div>
553
+
554
+ <!-- 邮箱 -->
555
+ <div class="chat-settings-form-item">
556
+ <label class="chat-settings-form-label">邮箱</label>
557
+ <el-input
558
+ v-if="isEditingUserInfo"
559
+ v-model="editingUserInfo.email"
560
+ placeholder="请输入邮箱"
561
+ size="large"
562
+ />
563
+ <div
564
+ v-else
565
+ class="chat-settings-form-value"
566
+ >
567
+ {{ userInfo.email || '未设置' }}
568
+ </div>
569
+ </div>
570
+
571
+ <!-- 手机号 -->
572
+ <div class="chat-settings-form-item">
573
+ <label class="chat-settings-form-label">手机号</label>
574
+ <el-input
575
+ v-if="isEditingUserInfo"
576
+ v-model="editingUserInfo.phone"
577
+ placeholder="请输入手机号"
578
+ size="large"
579
+ />
580
+ <div
581
+ v-else
582
+ class="chat-settings-form-value"
583
+ >
584
+ {{ userInfo.phone || '未设置' }}
585
+ </div>
586
+ </div>
587
+
588
+ <!-- 个人简介 -->
589
+ <div class="chat-settings-form-item">
590
+ <label class="chat-settings-form-label">个人简介</label>
591
+ <el-input
592
+ v-if="isEditingUserInfo"
593
+ v-model="editingUserInfo.bio"
594
+ type="textarea"
595
+ :rows="4"
596
+ placeholder="介绍一下自己吧..."
597
+ size="large"
598
+ />
599
+ <div
600
+ v-else
601
+ class="chat-settings-form-value bio-value"
602
+ >
603
+ {{ userInfo.bio || '这个人很懒,什么都没写~' }}
604
+ </div>
605
+ </div>
606
+
607
+ <!-- 编辑按钮 -->
608
+ <div v-if="isEditingUserInfo" class="chat-settings-form-actions">
609
+ <el-button size="default" @click="cancelEditUserInfo">取消</el-button>
610
+ <el-button
611
+ type="primary"
612
+ size="default"
613
+ :loading="savingUserInfo"
614
+ @click="saveUserInfo"
615
+ >保存更改</el-button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ </el-dialog>
621
+
622
+ <!-- 头像裁剪弹窗 -->
623
+ <AvatarCrop
624
+ v-model="showAvatarEditor"
625
+ :src="avatarImageSrc"
626
+ @confirm="handleAvatarCropConfirm"
627
+ />
628
+
629
+ <!-- 右键菜单 -->
630
+ <div
631
+ v-if="contextMenu.visible"
632
+ class="chat-context-menu"
633
+ :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
634
+ >
635
+ <div class="chat-context-menu-item" @click="handleRemoveChat">删除聊天</div>
636
+ </div>
637
+ </div>
638
+ </template>
639
+
640
+ <script setup>
641
+ import { computed, watch, nextTick, ref, onMounted, onUnmounted } from 'vue'
642
+ import {
643
+ Camera,
644
+ ChatDotRound,
645
+ Folder,
646
+ Picture,
647
+ ChatLineRound,
648
+ MoreFilled,
649
+ Search,
650
+ Plus,
651
+ UserFilled,
652
+ Bell,
653
+ Setting,
654
+ Document,
655
+ Download
656
+ } from '@element-plus/icons-vue'
657
+ import { useChat } from '../composables/useChat.js'
658
+ import { ElMessage } from 'element-plus'
659
+ import AvatarCrop from './AvatarCrop.vue'
660
+
661
+ const props = defineProps({
662
+ config: { type: Object, required: true }
663
+ })
664
+
665
+ const emit = defineEmits(['message', 'send', 'error', 'init'])
666
+
667
+ const {
668
+ myUsername,
669
+ myAvatar,
670
+ userInfo,
671
+ loadingUserInfo,
672
+ friendList,
673
+ filteredFriendList,
674
+ searchText,
675
+ inputText,
676
+ messagesContainer,
677
+ filteredUsers,
678
+ filteredAvailableUsers,
679
+ currentUser,
680
+ currentMessages,
681
+ addFriendDialogVisible,
682
+ addFriendSearchText,
683
+ availableUsers,
684
+ loadingAvailableUsers,
685
+ friendApplyList,
686
+ loadingFriendApply,
687
+ formatTime,
688
+ formatLastTime,
689
+ scrollToBottom,
690
+ getFriendList,
691
+ getChatHistory,
692
+ setFriendToChatStatus,
693
+ selectUser,
694
+ sendMessage,
695
+ sendFile,
696
+ sendFilesAndText,
697
+ initWebSocket,
698
+ closeWebSocket,
699
+ reset,
700
+ openAddFriendDialog,
701
+ addFriend,
702
+ loadFriendApplyList,
703
+ agreeFriend,
704
+ updateMyAvatar,
705
+ getUserInfo,
706
+ updateUserInfo
707
+ } = useChat(props.config, (message) => {
708
+ emit('message', message)
709
+ })
710
+
711
+ const navTabs = computed(() => {
712
+ const tabs = [{ id: 'chat', icon: ChatDotRound, badge: 0 }]
713
+
714
+ if (props.config.modules.friends) {
715
+ tabs.push({ id: 'friends', icon: UserFilled, badge: 0 })
716
+ }
717
+
718
+ if (props.config.modules.apply) {
719
+ tabs.push({ id: 'apply', icon: Bell, badge: friendApplyList.value?.length || 0 })
720
+ }
721
+
722
+ return tabs
723
+ })
724
+
725
+ const currentNavTab = ref('chat')
726
+ const currentChatId = ref(null)
727
+ const currentChat = ref(null)
728
+ const currentSelectedFriend = ref(null)
729
+ const showChatDetail = ref(false)
730
+ const isEditingUserInfo = ref(false)
731
+ const editingUserInfo = ref({ nickname: '', email: '', phone: '', bio: '' })
732
+ const savingUserInfo = ref(false)
733
+ const showSettingsDialog = ref(false)
734
+ const showAvatarEditor = ref(false)
735
+ const avatarUploading = ref(false)
736
+ const avatarInputRef = ref(null)
737
+ const avatarImageSrc = ref('')
738
+ const fileInputRef = ref(null)
739
+ const pendingFiles = ref([])
740
+ const contextMenu = ref({ visible: false, x: 0, y: 0, chat: null })
741
+
742
+ const showContextMenuFn = (e, chat) => {
743
+ e.preventDefault()
744
+ e.stopPropagation()
745
+ contextMenu.value = { visible: true, x: e.clientX, y: e.clientY, chat }
746
+ }
747
+
748
+ const hideContextMenu = () => {
749
+ contextMenu.value.visible = false
750
+ }
751
+
752
+ const handleRemoveChat = async () => {
753
+ if (!contextMenu.value.chat) return
754
+ const success = await setFriendToChatStatus(contextMenu.value.chat.id, 0)
755
+ if (success) {
756
+ if (currentChatId.value === contextMenu.value.chat.id) {
757
+ currentChatId.value = null
758
+ currentChat.value = null
759
+ }
760
+ }
761
+ hideContextMenu()
762
+ }
763
+
764
+ const selectChat = (chat) => {
765
+ currentChatId.value = chat.id
766
+ currentChat.value = chat
767
+ currentSelectedFriend.value = null
768
+ showChatDetail.value = false
769
+ selectUser({
770
+ id: chat.id,
771
+ name: chat.name,
772
+ avatar: chat.avatar,
773
+ online: chat.online
774
+ })
775
+ }
776
+
777
+ const selectFriend = (friend) => {
778
+ currentSelectedFriend.value = friend
779
+ currentChatId.value = null
780
+ currentChat.value = null
781
+ }
782
+
783
+ const handleStartChat = async () => {
784
+ if (!currentSelectedFriend.value) return
785
+
786
+ const success = await setFriendToChatStatus(currentSelectedFriend.value.id)
787
+ if (success) {
788
+ currentNavTab.value = 'chat'
789
+ await nextTick()
790
+ const chatItem = filteredUsers.value.find(u => u.id === currentSelectedFriend.value.id)
791
+ if (chatItem) {
792
+ selectChat(chatItem)
793
+ }
794
+ currentSelectedFriend.value = null
795
+ }
796
+ }
797
+
798
+ const handleAvatarClick = () => {
799
+ showSettingsDialog.value = true
800
+ }
801
+
802
+ const triggerAvatarUpload = () => {
803
+ avatarInputRef.value?.click()
804
+ }
805
+
806
+ const triggerFileSelect = () => {
807
+ fileInputRef.value?.click()
808
+ }
809
+
810
+ const handleFileSelect = (e) => {
811
+ const files = Array.from(e.target.files || [])
812
+ if (files.length === 0) return
813
+
814
+ for (const file of files) {
815
+ if (file.size > 50 * 1024 * 1024) {
816
+ ElMessage.warning(`文件 ${file.name} 超过50MB,已跳过`)
817
+ continue
818
+ }
819
+
820
+ const previewUrl = URL.createObjectURL(file)
821
+ pendingFiles.value.push({
822
+ id: Date.now() + Math.random(),
823
+ file,
824
+ name: file.name,
825
+ size: file.size,
826
+ type: file.type,
827
+ previewUrl,
828
+ isImage: file.type.startsWith('image/')
829
+ })
830
+ }
831
+
832
+ if (fileInputRef.value) {
833
+ fileInputRef.value.value = ''
834
+ }
835
+ }
836
+
837
+ const removePendingFile = (index) => {
838
+ const file = pendingFiles.value[index]
839
+ if (file.previewUrl) {
840
+ URL.revokeObjectURL(file.previewUrl)
841
+ }
842
+ pendingFiles.value.splice(index, 1)
843
+ }
844
+
845
+ const formatFileSize = (bytes) => {
846
+ if (bytes === 0) return '0 B'
847
+ const k = 1024
848
+ const sizes = ['B', 'KB', 'MB', 'GB']
849
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
850
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
851
+ }
852
+
853
+ const handleSend = async () => {
854
+ if (!inputText.value.trim() && pendingFiles.value.length === 0) return
855
+
856
+ const filesToSend = [...pendingFiles.value]
857
+ const textToSend = inputText.value
858
+
859
+ inputText.value = ''
860
+ pendingFiles.value.forEach((file) => {
861
+ if (file.previewUrl) {
862
+ URL.revokeObjectURL(file.previewUrl)
863
+ }
864
+ })
865
+ pendingFiles.value = []
866
+
867
+ await sendFilesAndText(filesToSend, textToSend)
868
+ emit('send', { text: textToSend, files: filesToSend })
869
+ }
870
+
871
+ const handlePaste = (e) => {
872
+ const items = e.clipboardData?.items
873
+ if (!items) return
874
+
875
+ for (const item of items) {
876
+ if (item.kind === 'file') {
877
+ const file = item.getAsFile()
878
+ if (file) {
879
+ if (file.size > 50 * 1024 * 1024) {
880
+ ElMessage.warning(`文件 ${file.name} 超过50MB,已跳过`)
881
+ continue
882
+ }
883
+
884
+ const previewUrl = URL.createObjectURL(file)
885
+ pendingFiles.value.push({
886
+ id: Date.now() + Math.random(),
887
+ file,
888
+ name: file.name,
889
+ size: file.size,
890
+ type: file.type,
891
+ previewUrl,
892
+ isImage: file.type.startsWith('image/')
893
+ })
894
+ }
895
+ }
896
+ }
897
+ }
898
+
899
+ const openFile = (fileUrl) => {
900
+ if (!fileUrl) {
901
+ ElMessage.warning('文件地址无效')
902
+ return
903
+ }
904
+ window.open(fileUrl, '_blank')
905
+ }
906
+
907
+ const handleImageError = (e) => {
908
+ console.warn('图片加载失败', e)
909
+ }
910
+
911
+ const handleAvatarFileChange = (e) => {
912
+ const file = e.target.files[0]
913
+ if (!file) return
914
+
915
+ if (!file.type.startsWith('image/')) {
916
+ ElMessage.error('只能上传图片文件')
917
+ return
918
+ }
919
+
920
+ if (file.size > 5 * 1024 * 1024) {
921
+ ElMessage.error('图片大小不能超过 5MB')
922
+ return
923
+ }
924
+
925
+ const reader = new FileReader()
926
+ reader.onload = (event) => {
927
+ avatarImageSrc.value = event.target.result
928
+ showAvatarEditor.value = true
929
+ }
930
+ reader.readAsDataURL(file)
931
+ }
932
+
933
+ const handleAvatarCropConfirm = async ({ file }) => {
934
+ if (!file) return
935
+
936
+ avatarUploading.value = true
937
+ try {
938
+ const { ChatApi } = await import('../core/api.js')
939
+ const api = new ChatApi(props.config)
940
+ const res = await api.uploadAvatar(file, myUsername)
941
+ if (res.code === 200) {
942
+ ElMessage.success('头像上传成功')
943
+ updateMyAvatar(res.data)
944
+ resetAvatar()
945
+ } else {
946
+ ElMessage.error(res.msg || '头像上传失败')
947
+ }
948
+ } catch (error) {
949
+ console.error(error)
950
+ ElMessage.error('头像上传失败')
951
+ } finally {
952
+ avatarUploading.value = false
953
+ }
954
+ }
955
+
956
+ const resetAvatar = () => {
957
+ avatarImageSrc.value = ''
958
+ showAvatarEditor.value = false
959
+ if (avatarInputRef.value) {
960
+ avatarInputRef.value.value = ''
961
+ }
962
+ }
963
+
964
+ const startEditUserInfo = () => {
965
+ editingUserInfo.value = {
966
+ nickname: userInfo.value.nickname || '',
967
+ email: userInfo.value.email || '',
968
+ phone: userInfo.value.phone || '',
969
+ bio: userInfo.value.bio || ''
970
+ }
971
+ isEditingUserInfo.value = true
972
+ }
973
+
974
+ const cancelEditUserInfo = () => {
975
+ isEditingUserInfo.value = false
976
+ editingUserInfo.value = { nickname: '', email: '', phone: '', bio: '' }
977
+ }
978
+
979
+ const saveUserInfo = async () => {
980
+ savingUserInfo.value = true
981
+ try {
982
+ const success = await updateUserInfo(editingUserInfo.value)
983
+ if (success) {
984
+ ElMessage.success('保存成功')
985
+ isEditingUserInfo.value = false
986
+ } else {
987
+ ElMessage.error('保存失败')
988
+ }
989
+ } catch (error) {
990
+ console.error(error)
991
+ ElMessage.error('保存失败')
992
+ } finally {
993
+ savingUserInfo.value = false
994
+ }
995
+ }
996
+
997
+ // 生命周期
998
+ onMounted(async () => {
999
+ await Promise.all([getFriendList(), loadFriendApplyList(), getUserInfo()])
1000
+ initWebSocket()
1001
+ if (filteredUsers.value.length > 0) {
1002
+ selectChat(filteredUsers.value[0])
1003
+ }
1004
+ emit('init')
1005
+ document.addEventListener('click', hideContextMenu)
1006
+ })
1007
+
1008
+ onUnmounted(() => {
1009
+ reset()
1010
+ closeWebSocket()
1011
+ document.removeEventListener('click', hideContextMenu)
1012
+ })
1013
+ </script>
1014
+
1015
+ <style scoped>
1016
+ /* ========== 基础样式 ========== */
1017
+ .chat-panel {
1018
+ display: flex;
1019
+ height: 680px;
1020
+ background-color: white;
1021
+ overflow: hidden;
1022
+ border-radius: 12px;
1023
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
1024
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1025
+ }
1026
+
1027
+ /* ========== 左侧导航栏 ========== */
1028
+ .chat-sidebar {
1029
+ width: 64px;
1030
+ display: flex;
1031
+ flex-direction: column;
1032
+ align-items: center;
1033
+ gap: 8px;
1034
+ background-color: #f9fafb;
1035
+ border-right: 1px solid #e5e7eb;
1036
+ }
1037
+
1038
+ .sidebar-avatar {
1039
+ margin-top: 16px;
1040
+ margin-bottom: 16px;
1041
+ cursor: pointer;
1042
+ }
1043
+
1044
+ .sidebar-avatar-img {
1045
+ width: 40px;
1046
+ height: 40px;
1047
+ border-radius: 50%;
1048
+ border: 2px solid #e5e7eb;
1049
+ object-fit: cover;
1050
+ }
1051
+
1052
+ .sidebar-nav-item {
1053
+ width: 40px;
1054
+ height: 40px;
1055
+ display: flex;
1056
+ align-items: center;
1057
+ justify-content: center;
1058
+ cursor: pointer;
1059
+ border-radius: 8px;
1060
+ transition: all 0.2s;
1061
+ position: relative;
1062
+ }
1063
+
1064
+ .sidebar-nav-item-active {
1065
+ background-color: #dcfce7;
1066
+ color: #07c160;
1067
+ }
1068
+
1069
+ .sidebar-nav-item-inactive {
1070
+ color: #6b7280;
1071
+ }
1072
+
1073
+ .sidebar-nav-item-inactive:hover {
1074
+ background-color: #f3f4f6;
1075
+ }
1076
+
1077
+ .sidebar-nav-badge {
1078
+ position: absolute;
1079
+ top: -4px;
1080
+ right: -4px;
1081
+ width: 16px;
1082
+ height: 16px;
1083
+ background-color: #ef4444;
1084
+ border-radius: 50%;
1085
+ font-size: 10px;
1086
+ color: white;
1087
+ display: flex;
1088
+ align-items: center;
1089
+ justify-content: center;
1090
+ }
1091
+
1092
+ .sidebar-spacer {
1093
+ flex: 1;
1094
+ }
1095
+
1096
+ /* ========== 中间内容面板 ========== */
1097
+ .chat-content-panel {
1098
+ width: 288px;
1099
+ background-color: #f5f5f5;
1100
+ border-right: 1px solid #e5e7eb;
1101
+ display: flex;
1102
+ flex-direction: column;
1103
+ }
1104
+
1105
+ .chat-search-bar {
1106
+ padding: 12px;
1107
+ }
1108
+
1109
+ .chat-search-input {
1110
+ width: 100%;
1111
+ }
1112
+
1113
+ .chat-content-scroll {
1114
+ flex: 1;
1115
+ overflow-y: auto;
1116
+ min-height: 0;
1117
+ }
1118
+
1119
+ /* 聊天列表项 */
1120
+ .chat-list-item {
1121
+ display: flex;
1122
+ align-items: center;
1123
+ padding: 12px;
1124
+ cursor: pointer;
1125
+ transition: background-color 0.2s;
1126
+ }
1127
+
1128
+ .chat-list-item:hover {
1129
+ background-color: #e5e5e5;
1130
+ }
1131
+
1132
+ .chat-list-item-active {
1133
+ background-color: #d6d6d6;
1134
+ }
1135
+
1136
+ .chat-list-avatar-wrapper {
1137
+ position: relative;
1138
+ flex-shrink: 0;
1139
+ }
1140
+
1141
+ .chat-list-avatar {
1142
+ width: 44px;
1143
+ height: 44px;
1144
+ border-radius: 50%;
1145
+ object-fit: cover;
1146
+ }
1147
+
1148
+ .chat-list-online-indicator {
1149
+ position: absolute;
1150
+ bottom: 0;
1151
+ right: 0;
1152
+ width: 12px;
1153
+ height: 12px;
1154
+ background-color: #22c55e;
1155
+ border-radius: 50%;
1156
+ border: 2px solid white;
1157
+ }
1158
+
1159
+ .chat-list-online-indicator.chat-list-offline {
1160
+ background-color: #9ca3af;
1161
+ }
1162
+
1163
+ .chat-list-info {
1164
+ margin-left: 12px;
1165
+ flex: 1;
1166
+ overflow: hidden;
1167
+ }
1168
+
1169
+ .chat-list-header {
1170
+ display: flex;
1171
+ justify-content: space-between;
1172
+ align-items: center;
1173
+ }
1174
+
1175
+ .chat-list-name {
1176
+ font-weight: 500;
1177
+ color: #1f2937;
1178
+ font-size: 14px;
1179
+ }
1180
+
1181
+ .chat-list-time {
1182
+ font-size: 12px;
1183
+ color: #9ca3af;
1184
+ }
1185
+
1186
+ .chat-list-preview {
1187
+ display: flex;
1188
+ justify-content: space-between;
1189
+ align-items: center;
1190
+ margin-top: 4px;
1191
+ }
1192
+
1193
+ .chat-list-last-msg {
1194
+ font-size: 12px;
1195
+ color: #6b7280;
1196
+ overflow: hidden;
1197
+ text-overflow: ellipsis;
1198
+ white-space: nowrap;
1199
+ padding-right: 8px;
1200
+ flex: 1;
1201
+ }
1202
+
1203
+ .chat-list-unread {
1204
+ background-color: #ef4444;
1205
+ color: white;
1206
+ font-size: 10px;
1207
+ border-radius: 9999px;
1208
+ padding: 2px 6px;
1209
+ min-width: 18px;
1210
+ text-align: center;
1211
+ }
1212
+
1213
+ /* 添加好友 */
1214
+ .add-friend-section {
1215
+ padding: 12px;
1216
+ }
1217
+
1218
+ .add-friend-btn {
1219
+ display: flex;
1220
+ align-items: center;
1221
+ gap: 8px;
1222
+ padding: 8px;
1223
+ border-radius: 8px;
1224
+ cursor: pointer;
1225
+ }
1226
+
1227
+ .add-friend-btn:hover {
1228
+ background-color: #e5e5e5;
1229
+ }
1230
+
1231
+ .add-friend-icon {
1232
+ width: 44px;
1233
+ height: 44px;
1234
+ background-color: #07c160;
1235
+ border-radius: 8px;
1236
+ display: flex;
1237
+ align-items: center;
1238
+ justify-content: center;
1239
+ }
1240
+
1241
+ .add-friend-text {
1242
+ font-size: 14px;
1243
+ color: #1f2937;
1244
+ }
1245
+
1246
+ /* 好友申请 */
1247
+ .friend-request-item {
1248
+ display: flex;
1249
+ align-items: center;
1250
+ justify-content: space-between;
1251
+ padding: 12px;
1252
+ }
1253
+
1254
+ .friend-request-item:hover {
1255
+ background-color: #e5e5e5;
1256
+ }
1257
+
1258
+ .friend-request-info {
1259
+ display: flex;
1260
+ align-items: center;
1261
+ }
1262
+
1263
+ .friend-request-avatar {
1264
+ width: 44px;
1265
+ height: 44px;
1266
+ border-radius: 50%;
1267
+ object-fit: cover;
1268
+ }
1269
+
1270
+ .friend-request-details {
1271
+ margin-left: 12px;
1272
+ }
1273
+
1274
+ .friend-request-username {
1275
+ font-weight: 500;
1276
+ color: #1f2937;
1277
+ font-size: 14px;
1278
+ }
1279
+
1280
+ .friend-request-desc {
1281
+ font-size: 12px;
1282
+ color: #6b7280;
1283
+ margin-top: 4px;
1284
+ }
1285
+
1286
+ /* ========== 右侧聊天区域 ========== */
1287
+ .chat-main-area {
1288
+ flex: 1;
1289
+ display: flex;
1290
+ flex-direction: column;
1291
+ min-width: 0;
1292
+ background-color: white;
1293
+ }
1294
+
1295
+ /* 好友信息 */
1296
+ .friend-profile-area {
1297
+ flex: 1;
1298
+ display: flex;
1299
+ flex-direction: column;
1300
+ align-items: center;
1301
+ justify-content: center;
1302
+ padding: 32px;
1303
+ background-color: #f5f5f5;
1304
+ }
1305
+
1306
+ .profile-avatar {
1307
+ width: 96px;
1308
+ height: 96px;
1309
+ border-radius: 50%;
1310
+ object-fit: cover;
1311
+ margin-bottom: 24px;
1312
+ }
1313
+
1314
+ .profile-name {
1315
+ font-size: 20px;
1316
+ font-weight: 500;
1317
+ color: #1f2937;
1318
+ margin-bottom: 8px;
1319
+ }
1320
+
1321
+ .profile-status {
1322
+ display: flex;
1323
+ align-items: center;
1324
+ gap: 8px;
1325
+ margin-bottom: 32px;
1326
+ }
1327
+
1328
+ .profile-status-dot {
1329
+ width: 8px;
1330
+ height: 8px;
1331
+ border-radius: 50%;
1332
+ }
1333
+
1334
+ .profile-status-online {
1335
+ background-color: #22c55e;
1336
+ }
1337
+
1338
+ .profile-status-offline {
1339
+ background-color: #9ca3af;
1340
+ }
1341
+
1342
+ .profile-start-chat-btn {
1343
+ width: 160px;
1344
+ }
1345
+
1346
+ /* 聊天窗口 */
1347
+ .chat-window-area {
1348
+ flex: 1;
1349
+ display: flex;
1350
+ flex-direction: column;
1351
+ min-height: 0;
1352
+ }
1353
+
1354
+ .chat-window-header {
1355
+ height: 56px;
1356
+ border-bottom: 1px solid #e5e7eb;
1357
+ display: flex;
1358
+ align-items: center;
1359
+ justify-content: space-between;
1360
+ padding: 0 16px;
1361
+ background-color: white;
1362
+ }
1363
+
1364
+ .chat-window-title {
1365
+ display: flex;
1366
+ align-items: center;
1367
+ gap: 12px;
1368
+ }
1369
+
1370
+ .chat-window-name {
1371
+ font-weight: 500;
1372
+ color: #1f2937;
1373
+ }
1374
+
1375
+ .chat-window-status {
1376
+ font-size: 12px;
1377
+ padding: 2px 8px;
1378
+ border-radius: 4px;
1379
+ }
1380
+
1381
+ .chat-window-status-online {
1382
+ background-color: #dcfce7;
1383
+ color: #16a34a;
1384
+ }
1385
+
1386
+ .chat-window-status-offline {
1387
+ background-color: #f3f4f6;
1388
+ color: #6b7280;
1389
+ }
1390
+
1391
+ .chat-window-actions {
1392
+ display: flex;
1393
+ align-items: center;
1394
+ gap: 12px;
1395
+ color: #6b7280;
1396
+ }
1397
+
1398
+ .chat-action-icon {
1399
+ cursor: pointer;
1400
+ }
1401
+
1402
+ .chat-action-icon:hover {
1403
+ color: #374151;
1404
+ }
1405
+
1406
+ /* 消息容器 */
1407
+ .chat-messages-container {
1408
+ flex: 1;
1409
+ overflow-y: auto;
1410
+ padding: 16px;
1411
+ background-color: #f5f5f5;
1412
+ min-height: 0;
1413
+ }
1414
+
1415
+ .chat-messages-container::-webkit-scrollbar {
1416
+ width: 6px;
1417
+ }
1418
+
1419
+ .chat-messages-container::-webkit-scrollbar-thumb {
1420
+ background: #ccc;
1421
+ border-radius: 3px;
1422
+ }
1423
+
1424
+ .chat-messages-container::-webkit-scrollbar-track {
1425
+ background: transparent;
1426
+ }
1427
+
1428
+ /* 消息样式 */
1429
+ .message-wrapper {
1430
+ display: flex;
1431
+ margin-bottom: 24px;
1432
+ align-items: flex-start;
1433
+ }
1434
+
1435
+ .message-self {
1436
+ flex-direction: row-reverse;
1437
+ }
1438
+
1439
+ .message-other {
1440
+ flex-direction: row;
1441
+ }
1442
+
1443
+ .message-avatar {
1444
+ flex-shrink: 0;
1445
+ }
1446
+
1447
+ .message-avatar-img {
1448
+ width: 40px;
1449
+ height: 40px;
1450
+ border-radius: 8px;
1451
+ object-fit: cover;
1452
+ }
1453
+
1454
+ .message-content {
1455
+ display: flex;
1456
+ flex-direction: column;
1457
+ max-width: 75%;
1458
+ }
1459
+
1460
+ .message-content-self {
1461
+ margin-right: 12px;
1462
+ align-items: flex-end;
1463
+ }
1464
+
1465
+ .message-content-other {
1466
+ margin-left: 12px;
1467
+ align-items: flex-start;
1468
+ }
1469
+
1470
+ .message-sender-name {
1471
+ font-size: 12px;
1472
+ color: #6b7280;
1473
+ margin-bottom: 4px;
1474
+ margin-left: 4px;
1475
+ }
1476
+
1477
+ .message-bubble-wrapper {
1478
+ position: relative;
1479
+ }
1480
+
1481
+ .message-bubble {
1482
+ padding: 8px 12px;
1483
+ font-size: 14px;
1484
+ word-break: break-all;
1485
+ white-space: pre-wrap;
1486
+ border-radius: 8px;
1487
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1488
+ }
1489
+
1490
+ .message-bubble-self {
1491
+ background-color: #95ec69;
1492
+ color: #1f2937;
1493
+ position: relative;
1494
+ }
1495
+
1496
+ .message-bubble-self::after {
1497
+ content: '';
1498
+ position: absolute;
1499
+ right: -5px;
1500
+ top: 10px;
1501
+ width: 10px;
1502
+ height: 10px;
1503
+ background-color: #95ec69;
1504
+ transform: rotate(45deg);
1505
+ box-shadow: 2px -2px 2px 0 rgba(0, 0, 0, 0.05);
1506
+ }
1507
+
1508
+ .message-bubble-other {
1509
+ background-color: white;
1510
+ color: #1f2937;
1511
+ position: relative;
1512
+ }
1513
+
1514
+ .message-bubble-other::after {
1515
+ content: '';
1516
+ position: absolute;
1517
+ left: -5px;
1518
+ top: 10px;
1519
+ width: 10px;
1520
+ height: 10px;
1521
+ background-color: white;
1522
+ transform: rotate(45deg);
1523
+ box-shadow: -2px 2px 2px 0 rgba(0, 0, 0, 0.05);
1524
+ }
1525
+
1526
+ /* 图片消息 */
1527
+ .message-image-bubble {
1528
+ border-radius: 8px;
1529
+ position: relative;
1530
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1531
+ cursor: pointer;
1532
+ overflow: hidden;
1533
+ max-width: 300px;
1534
+ padding: 0;
1535
+ }
1536
+
1537
+ .message-image {
1538
+ width: 100%;
1539
+ height: auto;
1540
+ display: block;
1541
+ }
1542
+
1543
+ .message-image-size {
1544
+ position: absolute;
1545
+ left: 4px;
1546
+ bottom: 0;
1547
+ color: white;
1548
+ font-size: 10px;
1549
+ }
1550
+
1551
+ /* 文件消息 */
1552
+ .message-file-bubble {
1553
+ border-radius: 8px;
1554
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1555
+ cursor: pointer;
1556
+ overflow: hidden;
1557
+ min-width: 200px;
1558
+ }
1559
+
1560
+ .message-file-content {
1561
+ display: flex;
1562
+ align-items: center;
1563
+ gap: 12px;
1564
+ padding: 12px 16px;
1565
+ }
1566
+
1567
+ .message-file-icon {
1568
+ width: 40px;
1569
+ height: 40px;
1570
+ display: flex;
1571
+ align-items: center;
1572
+ justify-content: center;
1573
+ border-radius: 8px;
1574
+ flex-shrink: 0;
1575
+ }
1576
+
1577
+ .message-bubble-self .message-file-icon {
1578
+ color: #374151;
1579
+ }
1580
+
1581
+ .message-bubble-other .message-file-icon {
1582
+ color: #6b7280;
1583
+ }
1584
+
1585
+ .message-file-info {
1586
+ flex: 1;
1587
+ min-width: 0;
1588
+ }
1589
+
1590
+ .message-file-name {
1591
+ overflow: hidden;
1592
+ text-overflow: ellipsis;
1593
+ font-size: 14px;
1594
+ font-weight: 500;
1595
+ line-height: 1.2;
1596
+ }
1597
+
1598
+ .message-bubble-self .message-file-name {
1599
+ color: #1f2937;
1600
+ }
1601
+
1602
+ .message-bubble-other .message-file-name {
1603
+ color: #1f2937;
1604
+ }
1605
+
1606
+ .message-file-meta {
1607
+ font-size: 12px;
1608
+ margin-top: 4px;
1609
+ display: flex;
1610
+ align-items: center;
1611
+ gap: 8px;
1612
+ }
1613
+
1614
+ .message-bubble-self .message-file-meta {
1615
+ color: #4b5563;
1616
+ }
1617
+
1618
+ .message-bubble-other .message-file-meta {
1619
+ color: #6b7280;
1620
+ }
1621
+
1622
+ .message-time {
1623
+ font-size: 10px;
1624
+ color: #9ca3af;
1625
+ margin-top: 4px;
1626
+ }
1627
+
1628
+ .message-time-right {
1629
+ text-align: right;
1630
+ }
1631
+
1632
+ .message-time-left {
1633
+ text-align: left;
1634
+ }
1635
+
1636
+ /* 输入区域 */
1637
+ .chat-input-area {
1638
+ background-color: white;
1639
+ border-top: 1px solid #e5e7eb;
1640
+ }
1641
+
1642
+ .pending-files-area {
1643
+ padding: 8px 12px;
1644
+ border-bottom: 1px solid #f3f4f6;
1645
+ display: flex;
1646
+ flex-wrap: wrap;
1647
+ gap: 8px;
1648
+ }
1649
+
1650
+ .pending-file-item {
1651
+ position: relative;
1652
+ }
1653
+
1654
+ .pending-image-wrapper {
1655
+ position: relative;
1656
+ width: 80px;
1657
+ height: 80px;
1658
+ border-radius: 8px;
1659
+ overflow: hidden;
1660
+ border: 1px solid #e5e7eb;
1661
+ }
1662
+
1663
+ .pending-image {
1664
+ width: 100%;
1665
+ height: 100%;
1666
+ object-fit: cover;
1667
+ }
1668
+
1669
+ .pending-file-wrapper {
1670
+ position: relative;
1671
+ width: 96px;
1672
+ height: 80px;
1673
+ border-radius: 8px;
1674
+ border: 1px solid #e5e7eb;
1675
+ background-color: #f9fafb;
1676
+ display: flex;
1677
+ flex-direction: column;
1678
+ align-items: center;
1679
+ justify-content: center;
1680
+ padding: 4px;
1681
+ }
1682
+
1683
+ .pending-file-icon {
1684
+ color: #9ca3af;
1685
+ font-size: 28px;
1686
+ margin-bottom: 4px;
1687
+ }
1688
+
1689
+ .pending-file-name {
1690
+ font-size: 10px;
1691
+ color: #6b7280;
1692
+ overflow: hidden;
1693
+ text-overflow: ellipsis;
1694
+ white-space: nowrap;
1695
+ width: 100%;
1696
+ text-align: center;
1697
+ padding: 0 4px;
1698
+ }
1699
+
1700
+ .pending-file-remove-btn {
1701
+ position: absolute;
1702
+ top: 4px;
1703
+ right: 4px;
1704
+ width: 20px;
1705
+ height: 20px;
1706
+ background-color: rgba(0, 0, 0, 0.5);
1707
+ color: white;
1708
+ border-radius: 50%;
1709
+ display: flex;
1710
+ align-items: center;
1711
+ justify-content: center;
1712
+ cursor: pointer;
1713
+ transition: background-color 0.2s;
1714
+ font-size: 14px;
1715
+ border: none;
1716
+ }
1717
+
1718
+ .pending-file-remove-btn:hover {
1719
+ background-color: rgba(0, 0, 0, 0.7);
1720
+ }
1721
+
1722
+ .input-toolbar {
1723
+ display: flex;
1724
+ align-items: center;
1725
+ padding: 12px;
1726
+ gap: 8px;
1727
+ }
1728
+
1729
+ .input-toolbar-icon {
1730
+ color: #6b7280;
1731
+ cursor: pointer;
1732
+ font-size: 20px;
1733
+ }
1734
+
1735
+ .input-toolbar-icon:hover {
1736
+ color: #374151;
1737
+ }
1738
+
1739
+ .input-textarea-wrapper {
1740
+ padding: 0 12px 12px;
1741
+ }
1742
+
1743
+ .message-input-textarea {
1744
+ width: 100%;
1745
+ resize: none;
1746
+ border: none;
1747
+ outline: none;
1748
+ font-size: 14px;
1749
+ height: 80px;
1750
+ font-family: inherit;
1751
+ }
1752
+
1753
+ .input-send-wrapper {
1754
+ display: flex;
1755
+ justify-content: flex-end;
1756
+ padding: 0 12px 12px;
1757
+ }
1758
+
1759
+ .send-message-btn {
1760
+ background-color: #07c160;
1761
+ border: none;
1762
+ font-size: 14px;
1763
+ padding: 8px 24px;
1764
+ }
1765
+
1766
+ .send-message-btn:hover {
1767
+ background-color: #06ad56;
1768
+ }
1769
+
1770
+ .hidden-file-input {
1771
+ display: none;
1772
+ }
1773
+
1774
+ /* 空状态 */
1775
+ .chat-empty-state {
1776
+ flex: 1;
1777
+ display: flex;
1778
+ align-items: center;
1779
+ justify-content: center;
1780
+ flex-direction: column;
1781
+ background-color: #f5f5f5;
1782
+ }
1783
+
1784
+ .empty-state-icon {
1785
+ color: #d1d5db;
1786
+ margin-bottom: 8px;
1787
+ }
1788
+
1789
+ .empty-state-text {
1790
+ color: #9ca3af;
1791
+ font-size: 14px;
1792
+ }
1793
+
1794
+ /* ========== 右侧详情面板 ========== */
1795
+ .chat-detail-panel {
1796
+ width: 256px;
1797
+ background-color: #f5f5f5;
1798
+ border-left: 1px solid #e5e7eb;
1799
+ display: flex;
1800
+ flex-direction: column;
1801
+ }
1802
+
1803
+ .chat-detail-header {
1804
+ height: 56px;
1805
+ display: flex;
1806
+ align-items: center;
1807
+ justify-content: center;
1808
+ border-bottom: 1px solid #e5e7eb;
1809
+ font-weight: 500;
1810
+ color: #374151;
1811
+ font-size: 14px;
1812
+ }
1813
+
1814
+ .chat-detail-content {
1815
+ flex: 1;
1816
+ padding: 16px;
1817
+ }
1818
+
1819
+ .chat-detail-profile {
1820
+ display: flex;
1821
+ flex-direction: column;
1822
+ align-items: center;
1823
+ }
1824
+
1825
+ .chat-detail-avatar {
1826
+ width: 80px;
1827
+ height: 80px;
1828
+ border-radius: 50%;
1829
+ object-fit: cover;
1830
+ }
1831
+
1832
+ .chat-detail-name {
1833
+ margin-top: 12px;
1834
+ font-weight: 500;
1835
+ color: #1f2937;
1836
+ font-size: 18px;
1837
+ }
1838
+
1839
+ .chat-detail-actions {
1840
+ margin-top: 24px;
1841
+ width: 100%;
1842
+ }
1843
+
1844
+ .chat-detail-action-item {
1845
+ padding: 12px;
1846
+ border-bottom: 1px solid #f3f4f6;
1847
+ cursor: pointer;
1848
+ background-color: white;
1849
+ font-size: 14px;
1850
+ color: #374151;
1851
+ }
1852
+
1853
+ .chat-detail-action-item:hover {
1854
+ background-color: #f9fafb;
1855
+ }
1856
+
1857
+ .chat-detail-action-item:first-child {
1858
+ border-radius: 8px 8px 0 0;
1859
+ }
1860
+
1861
+ .chat-detail-action-item:last-child {
1862
+ border-bottom: none;
1863
+ border-radius: 0 0 8px 8px;
1864
+ }
1865
+
1866
+ /* ========== 添加好友搜索 ========== */
1867
+ .add-friend-search-wrapper {
1868
+ margin-bottom: 16px;
1869
+ }
1870
+
1871
+ .add-friend-search-input {
1872
+ width: 100%;
1873
+ }
1874
+
1875
+ .add-friend-users-list {
1876
+ max-height: 400px;
1877
+ overflow-y: auto;
1878
+ }
1879
+
1880
+ .add-friend-user-item {
1881
+ display: flex;
1882
+ align-items: center;
1883
+ justify-content: space-between;
1884
+ padding: 12px;
1885
+ margin-bottom: 8px;
1886
+ border-radius: 8px;
1887
+ transition: background-color 0.2s;
1888
+ }
1889
+
1890
+ .add-friend-user-item:hover {
1891
+ background-color: #f9fafb;
1892
+ }
1893
+
1894
+ .add-friend-user-info {
1895
+ display: flex;
1896
+ align-items: center;
1897
+ }
1898
+
1899
+ .add-friend-user-avatar {
1900
+ width: 40px;
1901
+ height: 40px;
1902
+ border-radius: 50%;
1903
+ object-fit: cover;
1904
+ }
1905
+
1906
+ .add-friend-user-name {
1907
+ margin-left: 12px;
1908
+ font-weight: 500;
1909
+ color: #1f2937;
1910
+ font-size: 14px;
1911
+ }
1912
+
1913
+ /* ========== 设置弹窗 ========== */
1914
+ .chat-settings-dialog :deep(.el-dialog) {
1915
+ border-radius: 16px;
1916
+ overflow: hidden;
1917
+ }
1918
+
1919
+ .chat-settings-dialog :deep(.el-dialog__header) {
1920
+ padding: 24px 24px 0;
1921
+ margin: 0;
1922
+ }
1923
+
1924
+ .chat-settings-dialog :deep(.el-dialog__title) {
1925
+ font-size: 20px;
1926
+ font-weight: 600;
1927
+ color: #1f2937;
1928
+ }
1929
+
1930
+ .chat-settings-container {
1931
+ display: flex;
1932
+ flex-direction: column;
1933
+ }
1934
+
1935
+ .chat-settings-avatar-section {
1936
+ display: flex;
1937
+ flex-direction: column;
1938
+ align-items: center;
1939
+ margin-bottom: 32px;
1940
+ padding-bottom: 24px;
1941
+ border-bottom: 1px solid #f3f4f6;
1942
+ }
1943
+
1944
+ .chat-settings-avatar-wrapper {
1945
+ position: relative;
1946
+ margin-bottom: 16px;
1947
+ }
1948
+
1949
+ .chat-settings-avatar {
1950
+ width: 112px;
1951
+ height: 112px;
1952
+ border-radius: 50%;
1953
+ object-fit: cover;
1954
+ border: 4px solid white;
1955
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
1956
+ }
1957
+
1958
+ .chat-settings-avatar-edit {
1959
+ position: absolute;
1960
+ bottom: -4px;
1961
+ right: -4px;
1962
+ width: 40px;
1963
+ height: 40px;
1964
+ background-color: #07c160;
1965
+ border-radius: 50%;
1966
+ display: flex;
1967
+ align-items: center;
1968
+ justify-content: center;
1969
+ cursor: pointer;
1970
+ transition: background-color 0.2s;
1971
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1972
+ }
1973
+
1974
+ .chat-settings-avatar-edit:hover {
1975
+ background-color: #06ad56;
1976
+ }
1977
+
1978
+ .chat-settings-avatar-icon {
1979
+ color: white;
1980
+ }
1981
+
1982
+ .hidden-avatar-input {
1983
+ display: none;
1984
+ }
1985
+
1986
+ .chat-settings-user-display {
1987
+ text-align: center;
1988
+ }
1989
+
1990
+ .chat-settings-nickname {
1991
+ font-weight: 600;
1992
+ color: #1f2937;
1993
+ font-size: 20px;
1994
+ }
1995
+
1996
+ .chat-settings-username {
1997
+ font-size: 14px;
1998
+ color: #6b7280;
1999
+ margin-top: 4px;
2000
+ }
2001
+
2002
+ .chat-settings-form-section {
2003
+ gap: 20px;
2004
+ }
2005
+
2006
+ .chat-settings-form-header {
2007
+ display: flex;
2008
+ align-items: center;
2009
+ justify-content: space-between;
2010
+ margin-bottom: 8px;
2011
+ }
2012
+
2013
+ .chat-settings-form-title {
2014
+ color: #374151;
2015
+ font-weight: 600;
2016
+ display: flex;
2017
+ align-items: center;
2018
+ gap: 8px;
2019
+ font-size: 14px;
2020
+ }
2021
+
2022
+ .chat-settings-edit-btn {
2023
+ border-radius: 9999px;
2024
+ }
2025
+
2026
+ .chat-settings-form {
2027
+ background-color: #f9fafb;
2028
+ border-radius: 16px;
2029
+ padding: 24px;
2030
+ gap: 20px;
2031
+ display: flex;
2032
+ flex-direction: column;
2033
+ }
2034
+
2035
+ .chat-settings-form-item {
2036
+ display: flex;
2037
+ flex-direction: column;
2038
+ }
2039
+
2040
+ .chat-settings-form-label {
2041
+ display: block;
2042
+ font-size: 14px;
2043
+ color: #4b5563;
2044
+ margin-bottom: 8px;
2045
+ font-weight: 500;
2046
+ }
2047
+
2048
+ .chat-settings-form-value {
2049
+ color: #1f2937;
2050
+ background-color: white;
2051
+ border-radius: 8px;
2052
+ padding: 12px 16px;
2053
+ border: 1px solid #e5e7eb;
2054
+ font-size: 14px;
2055
+ }
2056
+
2057
+ .chat-settings-form-value.bio-value {
2058
+ min-height: 80px;
2059
+ }
2060
+
2061
+ .chat-settings-form-actions {
2062
+ display: flex;
2063
+ gap: 12px;
2064
+ justify-content: flex-end;
2065
+ padding-top: 8px;
2066
+ }
2067
+
2068
+ /* ========== 右键菜单 ========== */
2069
+ .chat-context-menu {
2070
+ position: fixed;
2071
+ background-color: white;
2072
+ border-radius: 8px;
2073
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
2074
+ border: 1px solid #e5e7eb;
2075
+ padding: 4px 0;
2076
+ z-index: 1000;
2077
+ }
2078
+
2079
+ .chat-context-menu-item {
2080
+ padding: 8px 16px;
2081
+ cursor: pointer;
2082
+ font-size: 14px;
2083
+ color: #374151;
2084
+ }
2085
+
2086
+ .chat-context-menu-item:hover {
2087
+ background-color: #f3f4f6;
2088
+ }
2089
+ </style>