vue-chat-kit 0.3.7 → 0.3.9

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