vue-chat-kit 0.3.8 → 0.3.10

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