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.
- package/README.md +214 -0
- package/package.json +59 -0
- package/src/components/AvatarCrop.vue +229 -0
- package/src/components/ChatWindow.vue +1326 -0
- package/src/composables/useChat.js +588 -0
- package/src/config/index.js +111 -0
- package/src/core/api.js +189 -0
- package/src/core/request.js +174 -0
- package/src/core/websocket.js +159 -0
- package/src/index.js +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Vue Chat Kit
|
|
2
|
+
|
|
3
|
+
一个功能完整的 Vue 3 聊天组件库,支持 WebSocket 实时通信、文件上传、好友管理等功能。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🎨 开箱即用的聊天界面
|
|
8
|
+
- 🔌 WebSocket 实时通信
|
|
9
|
+
- 📁 文件上传支持
|
|
10
|
+
- 👥 好友管理
|
|
11
|
+
- 📝 消息历史记录
|
|
12
|
+
- 🔧 高度可配置
|
|
13
|
+
- 📦 模块化设计
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
### 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install vue-chat-kit
|
|
21
|
+
# 或
|
|
22
|
+
yarn add vue-chat-kit
|
|
23
|
+
# 或
|
|
24
|
+
pnpm add vue-chat-kit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 基础使用
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<template>
|
|
31
|
+
<ChatWindow
|
|
32
|
+
v-model="visible"
|
|
33
|
+
:config="chatConfig"
|
|
34
|
+
/>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { ref } from 'vue'
|
|
39
|
+
import { ChatWindow, createChatConfig } from 'vue-chat-kit'
|
|
40
|
+
import 'vue-chat-kit/style'
|
|
41
|
+
|
|
42
|
+
const visible = ref(false)
|
|
43
|
+
|
|
44
|
+
// 配置
|
|
45
|
+
const chatConfig = createChatConfig({
|
|
46
|
+
// API 基础配置
|
|
47
|
+
api: {
|
|
48
|
+
baseUrl: 'http://your-api.com',
|
|
49
|
+
websocketUrl: 'ws://your-websocket.com'
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// 用户信息
|
|
53
|
+
user: {
|
|
54
|
+
username: 'user123',
|
|
55
|
+
avatar: 'https://example.com/avatar.jpg'
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// 模块配置
|
|
59
|
+
modules: {
|
|
60
|
+
friends: true, // 启用好友模块
|
|
61
|
+
apply: true, // 启用好友申请模块
|
|
62
|
+
settings: true, // 启用设置模块
|
|
63
|
+
fileUpload: true // 启用文件上传
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API 文档
|
|
70
|
+
|
|
71
|
+
### ChatWindow Props
|
|
72
|
+
|
|
73
|
+
| 参数 | 说明 | 类型 | 默认值 |
|
|
74
|
+
|------|------|------|--------|
|
|
75
|
+
| v-model | 控制弹窗显示/隐藏 | boolean | false |
|
|
76
|
+
| config | 聊天组件配置对象 | ChatConfig | - |
|
|
77
|
+
| width | 弹窗宽度 | string \| number | '1100px' |
|
|
78
|
+
|
|
79
|
+
### createChatConfig 配置项
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
{
|
|
83
|
+
// API 配置
|
|
84
|
+
api: {
|
|
85
|
+
baseUrl: '',
|
|
86
|
+
websocketUrl: '',
|
|
87
|
+
// 自定义 API 端点
|
|
88
|
+
endpoints: {
|
|
89
|
+
getFriends: '/chart/friends',
|
|
90
|
+
getHistory: '/chart/history',
|
|
91
|
+
setRead: '/chart/read',
|
|
92
|
+
sendMessage: '',
|
|
93
|
+
uploadFile: '/chart/upload/file',
|
|
94
|
+
addFriend: '/chart/friend/add',
|
|
95
|
+
getApplyList: '/chart/friend/applyList',
|
|
96
|
+
agreeFriend: '/chart/friend/agree',
|
|
97
|
+
setChatStatus: '/chart/friend/chat/status',
|
|
98
|
+
getAvailableUsers: '/chart/user/canAddFriend',
|
|
99
|
+
getUserInfo: '/user/info',
|
|
100
|
+
updateUserInfo: '/user/info',
|
|
101
|
+
getUserAvatar: '/user/getAvatar',
|
|
102
|
+
uploadAvatar: '/user/uploadAvatar'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// 用户信息
|
|
107
|
+
user: {
|
|
108
|
+
username: '',
|
|
109
|
+
avatar: '',
|
|
110
|
+
nickname: '',
|
|
111
|
+
email: '',
|
|
112
|
+
phone: '',
|
|
113
|
+
bio: ''
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// 模块开关
|
|
117
|
+
modules: {
|
|
118
|
+
friends: true,
|
|
119
|
+
apply: true,
|
|
120
|
+
settings: true,
|
|
121
|
+
fileUpload: true,
|
|
122
|
+
avatarCrop: true
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// 主题配置
|
|
126
|
+
theme: {
|
|
127
|
+
primaryColor: '#07c160',
|
|
128
|
+
selfMessageBg: '#95ec69',
|
|
129
|
+
otherMessageBg: '#ffffff'
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// 自定义请求头
|
|
133
|
+
headers: {
|
|
134
|
+
// 默认会自动添加 Authorization
|
|
135
|
+
// 'Authorization': 'Bearer xxx'
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// WebSocket 配置
|
|
139
|
+
websocket: {
|
|
140
|
+
maxReconnectAttempts: 5,
|
|
141
|
+
reconnectDelay: 3000
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// 文件配置
|
|
145
|
+
file: {
|
|
146
|
+
maxSize: 50 * 1024 * 1024, // 50MB
|
|
147
|
+
allowedTypes: ['*']
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 事件
|
|
153
|
+
|
|
154
|
+
| 事件名 | 说明 | 回调参数 |
|
|
155
|
+
|--------|------|----------|
|
|
156
|
+
| open | 聊天窗口打开 | - |
|
|
157
|
+
| close | 聊天窗口关闭 | - |
|
|
158
|
+
| message | 收到新消息 | message |
|
|
159
|
+
| send | 发送消息 | message |
|
|
160
|
+
| error | 错误事件 | error |
|
|
161
|
+
|
|
162
|
+
### 高级用法 - 自定义 API 实现
|
|
163
|
+
|
|
164
|
+
如果你想完全自定义 API 实现,可以传入自定义的 API 适配器:
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
const chatConfig = createChatConfig({
|
|
168
|
+
api: {
|
|
169
|
+
baseUrl: 'http://your-api.com',
|
|
170
|
+
websocketUrl: 'ws://your-websocket.com',
|
|
171
|
+
// 自定义适配器
|
|
172
|
+
adapter: {
|
|
173
|
+
async getFriends(currentUser) {
|
|
174
|
+
// 自定义实现
|
|
175
|
+
return []
|
|
176
|
+
},
|
|
177
|
+
async getHistory(fromUser, toUser) {
|
|
178
|
+
// 自定义实现
|
|
179
|
+
return []
|
|
180
|
+
},
|
|
181
|
+
async sendMessage(to, message, type) {
|
|
182
|
+
// 自定义实现
|
|
183
|
+
}
|
|
184
|
+
// ... 更多方法
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 模块说明
|
|
191
|
+
|
|
192
|
+
### 核心模块 (Core)
|
|
193
|
+
- WebSocket 连接管理
|
|
194
|
+
- 消息收发
|
|
195
|
+
- 消息历史
|
|
196
|
+
|
|
197
|
+
### 好友模块 (Friends)
|
|
198
|
+
- 好友列表
|
|
199
|
+
- 添加好友
|
|
200
|
+
- 好友申请
|
|
201
|
+
|
|
202
|
+
### 设置模块 (Settings)
|
|
203
|
+
- 用户信息编辑
|
|
204
|
+
- 头像上传
|
|
205
|
+
- 个人设置
|
|
206
|
+
|
|
207
|
+
### 文件模块 (File)
|
|
208
|
+
- 文件上传
|
|
209
|
+
- 文件预览
|
|
210
|
+
- 文件下载
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-chat-kit",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "一个功能完整的 Vue 3 聊天组件库,支持 WebSocket 实时通信、文件上传、好友管理等功能",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/vue-chat-kit.umd.js",
|
|
7
|
+
"module": "dist/vue-chat-kit.es.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/vue-chat-kit.es.js",
|
|
16
|
+
"require": "./dist/vue-chat-kit.umd.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./style": "./dist/style.css"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"preview": "vite preview",
|
|
25
|
+
"example": "vite --config vite.example.config.js",
|
|
26
|
+
"example:build": "vite build --config vite.example.config.js"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"vue",
|
|
30
|
+
"vue3",
|
|
31
|
+
"chat",
|
|
32
|
+
"websocket",
|
|
33
|
+
"im",
|
|
34
|
+
"component"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"vue": "^3.3.0",
|
|
40
|
+
"element-plus": "^2.0.0",
|
|
41
|
+
"@element-plus/icons-vue": "^2.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"dayjs": "^1.11.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@vitejs/plugin-vue": "^4.0.0",
|
|
48
|
+
"vite": "^5.0.0",
|
|
49
|
+
"sass": "^1.69.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": ""
|
|
54
|
+
},
|
|
55
|
+
"directories": {
|
|
56
|
+
"doc": "docs",
|
|
57
|
+
"example": "examples"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog
|
|
3
|
+
v-model="dialogVisible"
|
|
4
|
+
title="裁剪头像"
|
|
5
|
+
width="500px"
|
|
6
|
+
:close-on-click-modal="false"
|
|
7
|
+
@closed="handleClosed"
|
|
8
|
+
>
|
|
9
|
+
<div class="avatar-crop-container">
|
|
10
|
+
<div class="crop-area" ref="cropAreaRef">
|
|
11
|
+
<img
|
|
12
|
+
:src="imageSrc"
|
|
13
|
+
ref="imageRef"
|
|
14
|
+
class="crop-image"
|
|
15
|
+
@load="onImageLoad"
|
|
16
|
+
@mousedown="startDrag"
|
|
17
|
+
@touchstart="startDrag"
|
|
18
|
+
/>
|
|
19
|
+
<div class="crop-overlay">
|
|
20
|
+
<div class="crop-box">
|
|
21
|
+
<div class="crop-border" ref="cropBoxRef"></div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="isDragging" class="crop-mask"></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="zoom-controls">
|
|
27
|
+
<el-slider
|
|
28
|
+
v-model="scale"
|
|
29
|
+
:min="0.5"
|
|
30
|
+
:max="3"
|
|
31
|
+
:step="0.1"
|
|
32
|
+
:show-tooltip="false"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<template #footer>
|
|
37
|
+
<el-button @click="dialogVisible = false">取消</el-button>
|
|
38
|
+
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
|
39
|
+
</template>
|
|
40
|
+
</el-dialog>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup>
|
|
44
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
45
|
+
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
modelValue: { type: Boolean, default: false },
|
|
48
|
+
src: { type: String, default: '' }
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const emit = defineEmits(['update:modelValue', 'confirm'])
|
|
52
|
+
|
|
53
|
+
const dialogVisible = computed({
|
|
54
|
+
get: () => props.modelValue,
|
|
55
|
+
set: (val) => emit('update:modelValue', val)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const imageSrc = ref('')
|
|
59
|
+
const imageRef = ref(null)
|
|
60
|
+
const cropAreaRef = ref(null)
|
|
61
|
+
const cropBoxRef = ref(null)
|
|
62
|
+
const scale = ref(1)
|
|
63
|
+
const position = ref({ x: 0, y: 0 })
|
|
64
|
+
const isDragging = ref(false)
|
|
65
|
+
const startPos = ref({ x: 0, y: 0 })
|
|
66
|
+
const imageSize = ref({ width: 0, height: 0 })
|
|
67
|
+
|
|
68
|
+
watch(() => props.src, (val) => {
|
|
69
|
+
if (val) {
|
|
70
|
+
imageSrc.value = val
|
|
71
|
+
scale.value = 1
|
|
72
|
+
position.value = { x: 0, y: 0 }
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const onImageLoad = () => {
|
|
77
|
+
nextTick(() => {
|
|
78
|
+
if (imageRef.value && cropAreaRef.value) {
|
|
79
|
+
const img = imageRef.value
|
|
80
|
+
const area = cropAreaRef.value
|
|
81
|
+
const minSide = Math.min(area.clientWidth, area.clientHeight)
|
|
82
|
+
|
|
83
|
+
if (img.naturalWidth > img.naturalHeight) {
|
|
84
|
+
imageSize.value.height = minSide
|
|
85
|
+
imageSize.value.width = (img.naturalWidth / img.naturalHeight) * minSide
|
|
86
|
+
} else {
|
|
87
|
+
imageSize.value.width = minSide
|
|
88
|
+
imageSize.value.height = (img.naturalHeight / img.naturalWidth) * minSide
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
position.value = {
|
|
92
|
+
x: (minSide - imageSize.value.width) / 2,
|
|
93
|
+
y: (minSide - imageSize.value.height) / 2
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const startDrag = (e) => {
|
|
100
|
+
isDragging.value = true
|
|
101
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
102
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY
|
|
103
|
+
startPos.value = { x: clientX - position.value.x, y: clientY - position.value.y }
|
|
104
|
+
|
|
105
|
+
document.addEventListener('mousemove', onDrag)
|
|
106
|
+
document.addEventListener('mouseup', stopDrag)
|
|
107
|
+
document.addEventListener('touchmove', onDrag)
|
|
108
|
+
document.addEventListener('touchend', stopDrag)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const onDrag = (e) => {
|
|
112
|
+
if (!isDragging.value) return
|
|
113
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
114
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY
|
|
115
|
+
position.value = {
|
|
116
|
+
x: clientX - startPos.value.x,
|
|
117
|
+
y: clientY - startPos.value.y
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stopDrag = () => {
|
|
122
|
+
isDragging.value = false
|
|
123
|
+
document.removeEventListener('mousemove', onDrag)
|
|
124
|
+
document.removeEventListener('mouseup', stopDrag)
|
|
125
|
+
document.removeEventListener('touchmove', onDrag)
|
|
126
|
+
document.removeEventListener('touchend', stopDrag)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleConfirm = () => {
|
|
130
|
+
const canvas = document.createElement('canvas')
|
|
131
|
+
const ctx = canvas.getContext('2d')
|
|
132
|
+
const size = 200
|
|
133
|
+
canvas.width = size
|
|
134
|
+
canvas.height = size
|
|
135
|
+
|
|
136
|
+
const img = imageRef.value
|
|
137
|
+
const cropBox = cropBoxRef.value
|
|
138
|
+
const area = cropAreaRef.value
|
|
139
|
+
|
|
140
|
+
if (img && cropBox && area) {
|
|
141
|
+
const boxRect = cropBox.getBoundingClientRect()
|
|
142
|
+
const areaRect = area.getBoundingClientRect()
|
|
143
|
+
|
|
144
|
+
const cropX = (boxRect.left - areaRect.left - position.value.x) / scale.value
|
|
145
|
+
const cropY = (boxRect.top - areaRect.top - position.value.y) / scale.value
|
|
146
|
+
const cropSize = boxRect.width / scale.value
|
|
147
|
+
|
|
148
|
+
ctx.drawImage(
|
|
149
|
+
img,
|
|
150
|
+
cropX * (img.naturalWidth / imageSize.value.width),
|
|
151
|
+
cropY * (img.naturalHeight / imageSize.value.height),
|
|
152
|
+
cropSize * (img.naturalWidth / imageSize.value.width),
|
|
153
|
+
cropSize * (img.naturalHeight / imageSize.value.height),
|
|
154
|
+
0,
|
|
155
|
+
0,
|
|
156
|
+
size,
|
|
157
|
+
size
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
canvas.toBlob((blob) => {
|
|
161
|
+
emit('confirm', { file: blob, url: canvas.toDataURL('image/png') })
|
|
162
|
+
dialogVisible.value = false
|
|
163
|
+
}, 'image/png')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const handleClosed = () => {
|
|
168
|
+
imageSrc.value = ''
|
|
169
|
+
scale.value = 1
|
|
170
|
+
position.value = { x: 0, y: 0 }
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<style scoped>
|
|
175
|
+
.avatar-crop-container {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
gap: 20px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.crop-area {
|
|
182
|
+
position: relative;
|
|
183
|
+
width: 100%;
|
|
184
|
+
height: 300px;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
background: #f5f5f5;
|
|
187
|
+
border-radius: 8px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.crop-image {
|
|
191
|
+
position: absolute;
|
|
192
|
+
cursor: move;
|
|
193
|
+
user-select: none;
|
|
194
|
+
max-width: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.crop-overlay {
|
|
198
|
+
position: absolute;
|
|
199
|
+
inset: 0;
|
|
200
|
+
pointer-events: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.crop-box {
|
|
204
|
+
position: absolute;
|
|
205
|
+
top: 50%;
|
|
206
|
+
left: 50%;
|
|
207
|
+
transform: translate(-50%, -50%);
|
|
208
|
+
width: 200px;
|
|
209
|
+
height: 200px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.crop-border {
|
|
213
|
+
width: 100%;
|
|
214
|
+
height: 100%;
|
|
215
|
+
border: 2px solid #fff;
|
|
216
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
|
217
|
+
border-radius: 8px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.crop-mask {
|
|
221
|
+
position: absolute;
|
|
222
|
+
inset: 0;
|
|
223
|
+
cursor: move;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.zoom-controls {
|
|
227
|
+
padding: 0 20px;
|
|
228
|
+
}
|
|
229
|
+
</style>
|