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 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>