vue-chat-kit 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,588 @@
1
+ /**
2
+ * 聊天核心状态管理
3
+ */
4
+ import { ref, computed, nextTick } from 'vue'
5
+ import dayjs from 'dayjs'
6
+ import { ChatWebSocket } from '../core/websocket.js'
7
+ import { ChatApi } from '../core/api.js'
8
+
9
+ export function useChat(config) {
10
+ // ========== 配置 ==========
11
+ const api = new ChatApi(config)
12
+ let socket = null
13
+
14
+ // ========== 状态 ==========
15
+ const myUsername = config.user.username
16
+ const myAvatar = ref(config.user.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${myUsername}`)
17
+
18
+ // 用户信息
19
+ const userInfo = ref({
20
+ username: myUsername,
21
+ nickname: config.user.nickname || '',
22
+ email: config.user.email || '',
23
+ phone: config.user.phone || '',
24
+ bio: config.user.bio || ''
25
+ })
26
+ const loadingUserInfo = ref(false)
27
+
28
+ // 好友和聊天列表
29
+ const friendList = ref([])
30
+ const chatList = ref([])
31
+ const chatMsgList = ref([])
32
+ const currentSelectName = ref('')
33
+
34
+ // UI 状态
35
+ const searchText = ref('')
36
+ const inputText = ref('')
37
+ const messagesContainer = ref(null)
38
+
39
+ // 添加好友相关
40
+ const addFriendDialogVisible = ref(false)
41
+ const addFriendSearchText = ref('')
42
+ const availableUsers = ref([])
43
+ const loadingAvailableUsers = ref(false)
44
+
45
+ // 好友申请
46
+ const friendApplyList = ref([])
47
+ const loadingFriendApply = ref(false)
48
+
49
+ // ========== 计算属性 ==========
50
+
51
+ // 过滤后的聊天列表
52
+ const filteredUsers = computed(() => {
53
+ let list = chatList.value
54
+ if (searchText.value) {
55
+ const keyword = searchText.value.toLowerCase()
56
+ list = list.filter(item =>
57
+ item.username?.toLowerCase().includes(keyword)
58
+ )
59
+ }
60
+ return list.map(item => ({
61
+ id: item.username,
62
+ name: item.username,
63
+ avatar: item.avatar || myAvatar.value,
64
+ online: item.online,
65
+ lastMsg: item.lastMsg || '暂无消息',
66
+ lastTime: item.lastTime,
67
+ unread: item.unReadNum || 0
68
+ }))
69
+ })
70
+
71
+ // 过滤后的好友列表
72
+ const filteredFriendList = computed(() => {
73
+ let list = friendList.value
74
+ if (searchText.value) {
75
+ const keyword = searchText.value.toLowerCase()
76
+ list = list.filter(item =>
77
+ item.username?.toLowerCase().includes(keyword)
78
+ )
79
+ }
80
+ return list.map(item => ({
81
+ id: item.username,
82
+ name: item.username,
83
+ avatar: item.avatar || myAvatar.value,
84
+ online: item.online,
85
+ isChatting: item.isChatting
86
+ }))
87
+ })
88
+
89
+ // 过滤后的可添加用户
90
+ const filteredAvailableUsers = computed(() => {
91
+ if (!addFriendSearchText.value) return availableUsers.value
92
+ return availableUsers.value.filter(item =>
93
+ item.username?.toLowerCase().includes(addFriendSearchText.value.toLowerCase())
94
+ )
95
+ })
96
+
97
+ // 当前选中的用户
98
+ const currentUser = computed(() => {
99
+ return filteredUsers.value.find(item => item.id === currentSelectName.value) || null
100
+ })
101
+
102
+ // 处理后的消息列表
103
+ const currentMessages = computed(() => {
104
+ return chatMsgList.value.map(item => {
105
+ const isFileMessage = item.type === 'file' || item.fileUrl || item.fileName
106
+ const fileName = item.fileName || item.msgContent
107
+
108
+ return {
109
+ text: item.msgContent,
110
+ isSelf: item.sendUsername === myUsername,
111
+ time: item.createTime,
112
+ sendUsername: item.sendUsername,
113
+ type: isFileMessage ? 'file' : 'text',
114
+ fileType: isImageFile(fileName) ? 'image' : getFileIconType(fileName),
115
+ fileUrl: item.fileUrl || '',
116
+ fileName: fileName,
117
+ fileSize: item.fileSize || 0
118
+ }
119
+ })
120
+ })
121
+
122
+ // ========== 工具函数 ==========
123
+
124
+ const formatTime = (time) => dayjs(time).format('HH:mm')
125
+
126
+ const formatLastTime = (time) => {
127
+ if (!time) return ''
128
+ const now = dayjs()
129
+ const msgTime = dayjs(time)
130
+ if (now.isSame(msgTime, 'day')) return msgTime.format('HH:mm')
131
+ if (now.diff(msgTime, 'day') === 1) return '昨天'
132
+ if (now.diff(msgTime, 'day') < 7) {
133
+ const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
134
+ return weekDays[msgTime.day()]
135
+ }
136
+ return msgTime.format('MM/DD')
137
+ }
138
+
139
+ const isImageFile = (fileName) => {
140
+ if (!fileName) return false
141
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
142
+ const ext = fileName.split('.').pop().toLowerCase()
143
+ return imageExts.includes(ext)
144
+ }
145
+
146
+ const getFileIconType = (fileName) => {
147
+ if (!fileName) return 'default'
148
+ const ext = fileName.split('.').pop().toLowerCase()
149
+ if (['xls', 'xlsx'].includes(ext)) return 'excel'
150
+ if (['pdf'].includes(ext)) return 'pdf'
151
+ if (['doc', 'docx'].includes(ext)) return 'docx'
152
+ return 'default'
153
+ }
154
+
155
+ const scrollToBottom = () => {
156
+ nextTick(() => {
157
+ if (messagesContainer.value) {
158
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
159
+ }
160
+ })
161
+ }
162
+
163
+ // ========== API 方法 ==========
164
+
165
+ // 获取好友列表
166
+ const getFriendList = async () => {
167
+ try {
168
+ const res = await api.getFriends(myUsername)
169
+ const allFriends = res.data || []
170
+ friendList.value = allFriends
171
+ chatList.value = allFriends.filter(item => item.isChatting === 1)
172
+
173
+ // 获取未读数
174
+ for (const friend of chatList.value) {
175
+ try {
176
+ const historyRes = await api.getHistory(myUsername, friend.username)
177
+ const messages = historyRes.data || []
178
+ friend.unReadNum = messages.filter(msg =>
179
+ msg.isRead === 0 && msg.sendUsername === friend.username
180
+ ).length
181
+ } catch {
182
+ friend.unReadNum = 0
183
+ }
184
+ }
185
+ } catch (error) {
186
+ console.error('[VueChatKit] 获取好友列表失败', error)
187
+ }
188
+ }
189
+
190
+ // 获取聊天历史
191
+ const getChatHistory = async (targetName) => {
192
+ try {
193
+ const res = await api.getHistory(myUsername, targetName)
194
+ chatMsgList.value = res.data || []
195
+ scrollToBottom()
196
+ } catch (error) {
197
+ console.error('[VueChatKit] 获取聊天历史失败', error)
198
+ }
199
+ }
200
+
201
+ // 标记已读
202
+ const markAsRead = async (friendUser) => {
203
+ try {
204
+ await api.setRead(myUsername, friendUser)
205
+ getFriendList()
206
+ } catch (error) {
207
+ console.error('[VueChatKit] 标记已读失败', error)
208
+ }
209
+ }
210
+
211
+ // 选择聊天用户
212
+ const selectUser = async (user) => {
213
+ currentSelectName.value = user.id
214
+ await getChatHistory(user.id)
215
+ await markAsRead(user.id)
216
+ scrollToBottom()
217
+ }
218
+
219
+ // 设置好友聊天状态
220
+ const setFriendToChatStatus = async (friendUser, status = 1) => {
221
+ try {
222
+ await api.setChatStatus(myUsername, friendUser, status)
223
+ await getFriendList()
224
+ return true
225
+ } catch (error) {
226
+ console.error('[VueChatKit] 设置聊天状态失败', error)
227
+ return false
228
+ }
229
+ }
230
+
231
+ // 发送文本消息
232
+ const sendMessage = () => {
233
+ if (!inputText.value.trim() || !currentSelectName.value || !socket) return
234
+
235
+ const success = socket.send(currentSelectName.value, inputText.value.trim(), 'text')
236
+ if (success) {
237
+ const tempMsg = {
238
+ msgContent: inputText.value.trim(),
239
+ sendUsername: myUsername,
240
+ receiveUsername: currentSelectName.value,
241
+ createTime: new Date(),
242
+ isRead: 0,
243
+ type: 'text'
244
+ }
245
+ chatMsgList.value.push(tempMsg)
246
+ inputText.value = ''
247
+ scrollToBottom()
248
+ setTimeout(() => {
249
+ getChatHistory(currentSelectName.value)
250
+ getFriendList()
251
+ }, 300)
252
+ }
253
+ }
254
+
255
+ // 发送文件
256
+ const sendFile = async (file) => {
257
+ if (!currentSelectName.value || !socket) return false
258
+
259
+ try {
260
+ const uploadRes = await api.uploadFile(file)
261
+
262
+ if (uploadRes.code === 200 && uploadRes.data) {
263
+ const { fileUrl, fileName } = uploadRes.data
264
+
265
+ const success = socket.send(
266
+ currentSelectName.value,
267
+ fileName,
268
+ 'file',
269
+ fileUrl,
270
+ fileName,
271
+ file.size
272
+ )
273
+
274
+ if (success) {
275
+ const tempMsg = {
276
+ msgContent: fileName,
277
+ sendUsername: myUsername,
278
+ receiveUsername: currentSelectName.value,
279
+ createTime: new Date(),
280
+ isRead: 0,
281
+ type: 'file',
282
+ fileUrl: fileUrl,
283
+ fileName: fileName,
284
+ fileSize: file.size
285
+ }
286
+ chatMsgList.value.push(tempMsg)
287
+ scrollToBottom()
288
+ return true
289
+ }
290
+ }
291
+ return false
292
+ } catch (error) {
293
+ console.error('[VueChatKit] 发送文件失败', error)
294
+ return false
295
+ }
296
+ }
297
+
298
+ // 批量发送文件和文本
299
+ const sendFilesAndText = async (files, text) => {
300
+ if (!currentSelectName.value || !socket) return
301
+
302
+ if (text && text.trim()) {
303
+ const textSuccess = socket.send(currentSelectName.value, text.trim(), 'text')
304
+ if (textSuccess) {
305
+ const tempMsg = {
306
+ msgContent: text.trim(),
307
+ sendUsername: myUsername,
308
+ receiveUsername: currentSelectName.value,
309
+ createTime: new Date(),
310
+ isRead: 0,
311
+ type: 'text'
312
+ }
313
+ chatMsgList.value.push(tempMsg)
314
+ }
315
+ }
316
+
317
+ for (const fileObj of files) {
318
+ const file = fileObj.file || fileObj
319
+ await sendFile(file)
320
+ }
321
+
322
+ setTimeout(() => {
323
+ getChatHistory(currentSelectName.value)
324
+ getFriendList()
325
+ }, 300)
326
+ }
327
+
328
+ // ========== WebSocket 相关 ==========
329
+
330
+ // 解析 WebSocket 消息
331
+ const parseWsMessage = (data) => {
332
+ try {
333
+ try {
334
+ const jsonData = JSON.parse(data)
335
+ if (jsonData.to || jsonData.msg) {
336
+ return {
337
+ to: jsonData.to,
338
+ content: jsonData.msg,
339
+ type: jsonData.type || 'text',
340
+ fileUrl: jsonData.fileUrl || '',
341
+ fileName: jsonData.fileName || '',
342
+ fileSize: jsonData.fileSize || 0
343
+ }
344
+ }
345
+ } catch {
346
+ // 旧格式
347
+ }
348
+
349
+ const match = data.match(/^\[(.+?)\]:(.+)$/)
350
+ if (match) {
351
+ return {
352
+ username: match[1],
353
+ content: match[2],
354
+ type: 'text'
355
+ }
356
+ }
357
+ } catch (e) {
358
+ console.error('[VueChatKit] 解析消息失败', e)
359
+ }
360
+ return null
361
+ }
362
+
363
+ // 处理 WebSocket 消息
364
+ const handleWsMessage = (data) => {
365
+ if (data.includes('【状态变更】')) {
366
+ const reg = /【状态变更】(.+?) 已(上线|下线)/
367
+ const res = data.match(reg)
368
+ if (res) {
369
+ const targetName = res[1]
370
+ const isOnline = res[2] === '上线'
371
+ const targetFriend = friendList.value.find(item => item.username === targetName)
372
+ if (targetFriend) {
373
+ targetFriend.online = isOnline
374
+ }
375
+ }
376
+ return
377
+ }
378
+
379
+ const msgInfo = parseWsMessage(data)
380
+ if (msgInfo) {
381
+ if (currentSelectName.value) {
382
+ try {
383
+ let tempMsg = {
384
+ msgContent: msgInfo.content,
385
+ sendUsername: msgInfo.username || currentSelectName.value,
386
+ receiveUsername: myUsername,
387
+ createTime: new Date(),
388
+ isRead: 0,
389
+ type: msgInfo.type || 'text',
390
+ fileUrl: msgInfo.fileUrl || '',
391
+ fileName: msgInfo.fileName || '',
392
+ fileSize: msgInfo.fileSize || 0
393
+ }
394
+ chatMsgList.value.push(tempMsg)
395
+ scrollToBottom()
396
+ } catch (e) {
397
+ console.error('[VueChatKit] 添加临时消息失败', e)
398
+ }
399
+ getChatHistory(currentSelectName.value)
400
+ }
401
+ getFriendList()
402
+ }
403
+ }
404
+
405
+ // 初始化 WebSocket
406
+ const initWebSocket = () => {
407
+ const wsUrl = `${config.api.websocketUrl}?userId=${myUsername}`
408
+ socket = new ChatWebSocket(myUsername, {
409
+ wsUrl,
410
+ maxReconnectAttempts: config.websocket.maxReconnectAttempts,
411
+ reconnectDelay: config.websocket.reconnectDelay
412
+ })
413
+ socket.on('message', handleWsMessage)
414
+ socket.connect()
415
+ }
416
+
417
+ // 关闭 WebSocket
418
+ const closeWebSocket = () => {
419
+ if (socket) {
420
+ socket.close()
421
+ socket = null
422
+ }
423
+ }
424
+
425
+ // ========== 添加好友相关 ==========
426
+
427
+ const openAddFriendDialog = async () => {
428
+ addFriendDialogVisible.value = true
429
+ addFriendSearchText.value = ''
430
+ await loadAvailableUsers()
431
+ }
432
+
433
+ const loadAvailableUsers = async () => {
434
+ loadingAvailableUsers.value = true
435
+ try {
436
+ const res = await api.getAvailableUsers(myUsername)
437
+ availableUsers.value = res?.data || []
438
+ } catch (error) {
439
+ console.error('[VueChatKit] 获取可用用户失败', error)
440
+ } finally {
441
+ loadingAvailableUsers.value = false
442
+ }
443
+ }
444
+
445
+ const addFriend = async (user) => {
446
+ try {
447
+ await api.addFriend(myUsername, user.username)
448
+ await getFriendList()
449
+ addFriendDialogVisible.value = false
450
+ } catch (error) {
451
+ console.error('[VueChatKit] 添加好友失败', error)
452
+ }
453
+ }
454
+
455
+ const loadFriendApplyList = async () => {
456
+ loadingFriendApply.value = true
457
+ try {
458
+ const res = await api.getApplyList(myUsername)
459
+ friendApplyList.value = res.data || []
460
+ } catch (error) {
461
+ console.error('[VueChatKit] 获取好友申请列表失败', error)
462
+ } finally {
463
+ loadingFriendApply.value = false
464
+ }
465
+ }
466
+
467
+ const agreeFriend = async (applyUser) => {
468
+ try {
469
+ await api.agreeFriend(applyUser, myUsername)
470
+ await loadFriendApplyList()
471
+ await getFriendList()
472
+ } catch (error) {
473
+ console.error('[VueChatKit] 同意好友申请失败', error)
474
+ }
475
+ }
476
+
477
+ // ========== 用户信息相关 ==========
478
+
479
+ const initUserAvatar = async () => {
480
+ try {
481
+ const res = await api.getUserAvatar(myUsername)
482
+ if (res.code === 200 && res.data) {
483
+ myAvatar.value = res.data
484
+ }
485
+ } catch (error) {
486
+ console.warn('[VueChatKit] 加载头像失败', error)
487
+ }
488
+ }
489
+
490
+ const updateMyAvatar = (avatarUrl) => {
491
+ myAvatar.value = avatarUrl
492
+ }
493
+
494
+ const getUserInfo = async () => {
495
+ loadingUserInfo.value = true
496
+ try {
497
+ const res = await api.getUserInfo(myUsername)
498
+ if (res.code === 200 && res.data) {
499
+ userInfo.value = {
500
+ ...userInfo.value,
501
+ ...res.data
502
+ }
503
+ }
504
+ } catch (error) {
505
+ console.error('[VueChatKit] 获取用户信息失败', error)
506
+ } finally {
507
+ loadingUserInfo.value = false
508
+ }
509
+ }
510
+
511
+ const updateUserInfo = async (data) => {
512
+ try {
513
+ const res = await api.updateUserInfo(myUsername, data)
514
+ if (res.code === 200) {
515
+ userInfo.value = {
516
+ ...userInfo.value,
517
+ ...data
518
+ }
519
+ return true
520
+ }
521
+ return false
522
+ } catch (error) {
523
+ console.error('[VueChatKit] 更新用户信息失败', error)
524
+ return false
525
+ }
526
+ }
527
+
528
+ // 重置状态
529
+ const reset = () => {
530
+ currentSelectName.value = ''
531
+ chatMsgList.value = []
532
+ inputText.value = ''
533
+ searchText.value = ''
534
+ }
535
+
536
+ // 初始化头像
537
+ initUserAvatar()
538
+
539
+ return {
540
+ // 状态
541
+ myUsername,
542
+ myAvatar,
543
+ userInfo,
544
+ loadingUserInfo,
545
+ friendList,
546
+ chatList,
547
+ filteredFriendList,
548
+ chatMsgList,
549
+ currentSelectName,
550
+ searchText,
551
+ inputText,
552
+ messagesContainer,
553
+ filteredUsers,
554
+ filteredAvailableUsers,
555
+ currentUser,
556
+ currentMessages,
557
+ addFriendDialogVisible,
558
+ addFriendSearchText,
559
+ availableUsers,
560
+ loadingAvailableUsers,
561
+ friendApplyList,
562
+ loadingFriendApply,
563
+
564
+ // 方法
565
+ formatTime,
566
+ formatLastTime,
567
+ scrollToBottom,
568
+ getFriendList,
569
+ getChatHistory,
570
+ setFriendToChatStatus,
571
+ selectUser,
572
+ sendMessage,
573
+ sendFile,
574
+ sendFilesAndText,
575
+ initWebSocket,
576
+ closeWebSocket,
577
+ reset,
578
+ openAddFriendDialog,
579
+ addFriend,
580
+ loadFriendApplyList,
581
+ agreeFriend,
582
+ updateMyAvatar,
583
+ getUserInfo,
584
+ updateUserInfo
585
+ }
586
+ }
587
+
588
+ export default useChat
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 默认配置
3
+ */
4
+ const defaultConfig = {
5
+ // API 配置
6
+ api: {
7
+ baseUrl: '',
8
+ websocketUrl: '',
9
+ endpoints: {
10
+ getFriends: '/chart/friends',
11
+ getHistory: '/chart/history',
12
+ setRead: '/chart/read',
13
+ uploadFile: '/chart/upload/file',
14
+ addFriend: '/chart/friend/add',
15
+ getApplyList: '/chart/friend/applyList',
16
+ agreeFriend: '/chart/friend/agree',
17
+ setChatStatus: '/chart/friend/chat/status',
18
+ getAvailableUsers: '/chart/user/canAddFriend',
19
+ getUserInfo: '/user/info',
20
+ updateUserInfo: '/user/info',
21
+ getUserAvatar: '/user/getAvatar',
22
+ uploadAvatar: '/user/uploadAvatar'
23
+ },
24
+ adapter: null
25
+ },
26
+
27
+ // 用户信息
28
+ user: {
29
+ username: '',
30
+ avatar: '',
31
+ nickname: '',
32
+ email: '',
33
+ phone: '',
34
+ bio: ''
35
+ },
36
+
37
+ // 模块开关
38
+ modules: {
39
+ friends: true,
40
+ apply: true,
41
+ settings: true,
42
+ fileUpload: true,
43
+ avatarCrop: true
44
+ },
45
+
46
+ // 主题配置
47
+ theme: {
48
+ primaryColor: '#07c160',
49
+ selfMessageBg: '#95ec69',
50
+ otherMessageBg: '#ffffff'
51
+ },
52
+
53
+ // 自定义请求头
54
+ headers: {},
55
+
56
+ // WebSocket 配置
57
+ websocket: {
58
+ maxReconnectAttempts: 5,
59
+ reconnectDelay: 3000
60
+ },
61
+
62
+ // 文件配置
63
+ file: {
64
+ maxSize: 50 * 1024 * 1024, // 50MB
65
+ allowedTypes: ['*']
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 创建配置
71
+ * @param {Object} userConfig - 用户配置
72
+ * @returns {Object} 合并后的配置
73
+ */
74
+ export function createChatConfig(userConfig = {}) {
75
+ const config = deepMerge(defaultConfig, userConfig)
76
+
77
+ // 验证必需配置
78
+ if (!config.api.baseUrl) {
79
+ console.warn('[VueChatKit] 请配置 api.baseUrl')
80
+ }
81
+ if (!config.api.websocketUrl) {
82
+ console.warn('[VueChatKit] 请配置 api.websocketUrl')
83
+ }
84
+ if (!config.user.username) {
85
+ console.warn('[VueChatKit] 请配置 user.username')
86
+ }
87
+
88
+ return config
89
+ }
90
+
91
+ /**
92
+ * 深度合并对象
93
+ */
94
+ function deepMerge(target, source) {
95
+ const result = { ...target }
96
+
97
+ for (const key in source) {
98
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
99
+ result[key] = deepMerge(target[key] || {}, source[key])
100
+ } else {
101
+ result[key] = source[key]
102
+ }
103
+ }
104
+
105
+ return result
106
+ }
107
+
108
+ export default {
109
+ createChatConfig,
110
+ defaultConfig
111
+ }