@flexem/chat-box 1.0.0

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.
Files changed (46) hide show
  1. package/README.md +638 -0
  2. package/miniprogram_dist/TEST_CASES.md +256 -0
  3. package/miniprogram_dist/assets/icons/icon-arrow-down.svg +1 -0
  4. package/miniprogram_dist/assets/icons/icon-arrow-up.svg +1 -0
  5. package/miniprogram_dist/assets/icons/icon-avatar-default.svg +1 -0
  6. package/miniprogram_dist/assets/icons/icon-back.svg +1 -0
  7. package/miniprogram_dist/assets/icons/icon-camera.svg +1 -0
  8. package/miniprogram_dist/assets/icons/icon-close.svg +1 -0
  9. package/miniprogram_dist/assets/icons/icon-copy.svg +1 -0
  10. package/miniprogram_dist/assets/icons/icon-delete.svg +1 -0
  11. package/miniprogram_dist/assets/icons/icon-edit-msg.svg +1 -0
  12. package/miniprogram_dist/assets/icons/icon-edit.svg +1 -0
  13. package/miniprogram_dist/assets/icons/icon-file.svg +1 -0
  14. package/miniprogram_dist/assets/icons/icon-image.svg +1 -0
  15. package/miniprogram_dist/assets/icons/icon-keyboard.svg +1 -0
  16. package/miniprogram_dist/assets/icons/icon-menu.svg +1 -0
  17. package/miniprogram_dist/assets/icons/icon-play-voice.svg +1 -0
  18. package/miniprogram_dist/assets/icons/icon-plus.svg +1 -0
  19. package/miniprogram_dist/assets/icons/icon-regenerate.svg +1 -0
  20. package/miniprogram_dist/assets/icons/icon-thinking.svg +1 -0
  21. package/miniprogram_dist/assets/icons/icon-voice.svg +1 -0
  22. package/miniprogram_dist/components/attachment/index.js +169 -0
  23. package/miniprogram_dist/components/attachment/index.json +4 -0
  24. package/miniprogram_dist/components/attachment/index.wxml +40 -0
  25. package/miniprogram_dist/components/attachment/index.wxss +119 -0
  26. package/miniprogram_dist/components/input-bar/index.js +934 -0
  27. package/miniprogram_dist/components/input-bar/index.json +6 -0
  28. package/miniprogram_dist/components/input-bar/index.wxml +132 -0
  29. package/miniprogram_dist/components/input-bar/index.wxss +324 -0
  30. package/miniprogram_dist/components/message/index.js +988 -0
  31. package/miniprogram_dist/components/message/index.json +4 -0
  32. package/miniprogram_dist/components/message/index.wxml +285 -0
  33. package/miniprogram_dist/components/message/index.wxss +575 -0
  34. package/miniprogram_dist/components/sidebar/index.js +506 -0
  35. package/miniprogram_dist/components/sidebar/index.json +4 -0
  36. package/miniprogram_dist/components/sidebar/index.wxml +137 -0
  37. package/miniprogram_dist/components/sidebar/index.wxss +264 -0
  38. package/miniprogram_dist/index.js +1316 -0
  39. package/miniprogram_dist/index.json +8 -0
  40. package/miniprogram_dist/index.wxml +172 -0
  41. package/miniprogram_dist/index.wxss +291 -0
  42. package/miniprogram_dist/package.json +5 -0
  43. package/miniprogram_dist/utils/api.js +474 -0
  44. package/miniprogram_dist/utils/audio.js +860 -0
  45. package/miniprogram_dist/utils/storage.js +168 -0
  46. package/package.json +27 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {
4
+ "attachment-picker": "../attachment/index"
5
+ }
6
+ }
@@ -0,0 +1,132 @@
1
+ <view class="input-bar-container">
2
+ <!-- 附件预览区 -->
3
+ <view wx:if="{{attachments.length > 0}}" class="attachments-preview">
4
+ <view wx:for="{{attachments}}" wx:key="id" class="attachment-item">
5
+ <image
6
+ wx:if="{{item.type === 'image'}}"
7
+ class="attachment-thumb"
8
+ src="{{item.url}}"
9
+ mode="aspectFill"
10
+ bindtap="onPreviewAttachment"
11
+ data-url="{{item.url}}"
12
+ data-index="{{index}}"
13
+ />
14
+ <view wx:else class="attachment-file">
15
+ <text class="file-icon">📄</text>
16
+ <text class="file-name">{{item.name}}</text>
17
+ </view>
18
+ <view class="attachment-remove" catchtap="removeAttachment" data-id="{{item.id}}">
19
+ <text>×</text>
20
+ </view>
21
+ <view wx:if="{{item.uploading}}" class="attachment-progress">
22
+ <view class="progress-bar" style="width: {{item.progress}}%"></view>
23
+ </view>
24
+ </view>
25
+ </view>
26
+
27
+ <!-- 输入区域 -->
28
+ <view class="input-area">
29
+ <!-- 语音/键盘切换按钮 -->
30
+ <view class="mode-switch" bindtap="toggleInputMode">
31
+ <image
32
+ wx:if="{{isVoiceMode}}"
33
+ class="mode-icon"
34
+ src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-keyboard.svg"
35
+ mode="aspectFit"
36
+ />
37
+ <image
38
+ wx:else
39
+ class="mode-icon"
40
+ src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-voice.svg"
41
+ mode="aspectFit"
42
+ />
43
+ </view>
44
+
45
+ <!-- 文本输入模式 -->
46
+ <view wx:if="{{!isVoiceMode}}" class="text-input-wrapper">
47
+ <textarea
48
+ class="text-input"
49
+ placeholder="{{placeholder}}"
50
+ placeholder-class="text-input-placeholder"
51
+ value="{{inputValue}}"
52
+ focus="{{textareaFocus}}"
53
+ bindinput="onInput"
54
+ bindconfirm="onSend"
55
+ bindlinechange="onLineChange"
56
+ bindfocus="onInputFocus"
57
+ bindblur="onInputBlur"
58
+ bindkeyboardheightchange="onKeyboardHeightChange"
59
+ auto-height="true"
60
+ maxlength="{{maxLength}}"
61
+ confirm-type="send"
62
+ cursor-spacing="20"
63
+ show-confirm-bar="{{false}}"
64
+ adjust-position="{{false}}"
65
+ style="height: {{inputHeight}}rpx; max-height: {{maxInputHeight}}rpx;"
66
+ />
67
+ </view>
68
+
69
+ <!-- 语音输入模式 -->
70
+ <view
71
+ wx:else
72
+ class="voice-btn {{isRecording ? 'recording' : ''}} {{isCancelArea ? 'cancel' : ''}}"
73
+ bindtouchstart="onVoiceStart"
74
+ bindtouchmove="onVoiceMove"
75
+ bindtouchend="onVoiceEnd"
76
+ bindtouchcancel="onVoiceCancel"
77
+ bindlongpress="onVoiceLongPress"
78
+ >
79
+ <text>{{isCancelArea ? '松开 取消' : (isRecording ? '松开 结束' : '按住 说话')}}</text>
80
+ </view>
81
+
82
+ <!-- 附件按钮(生成中时隐藏) -->
83
+ <view wx:if="{{!isGenerating}}" class="attachment-btn" bindtap="showAttachmentPicker">
84
+ <image
85
+ class="mode-icon"
86
+ src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-plus.svg"
87
+ mode="aspectFit"
88
+ />
89
+ </view>
90
+
91
+ <!-- 发送/停止按钮 -->
92
+ <!-- 生成中时始终显示停止按钮,非生成中时只在文本模式且可发送时显示发送按钮 -->
93
+ <view
94
+ wx:if="{{isGenerating || (!isVoiceMode && canSend)}}"
95
+ class="send-btn {{isGenerating ? 'stop' : 'active'}}"
96
+ bindtap="{{isGenerating ? 'onStop' : 'onSend'}}"
97
+ >
98
+ <view wx:if="{{isGenerating}}" class="stop-icon"></view>
99
+ <text wx:else class="send-icon">➤</text>
100
+ </view>
101
+ </view>
102
+
103
+ <!-- 录音状态提示 -->
104
+ <view wx:if="{{isRecording}}" class="recording-overlay">
105
+ <view class="recording-content">
106
+ <view class="recording-animation">
107
+ <view class="wave wave1"></view>
108
+ <view class="wave wave2"></view>
109
+ <view class="wave wave3"></view>
110
+ </view>
111
+ <text class="recording-text">正在录音...</text>
112
+ <text class="recording-tip">松开发送,上滑取消</text>
113
+ </view>
114
+ </view>
115
+
116
+ <!-- 识别中提示 -->
117
+ <view wx:if="{{isRecognizing}}" class="recognizing-overlay">
118
+ <view class="recognizing-content">
119
+ <view class="recognizing-spinner"></view>
120
+ <text class="recognizing-text">语音识别中...</text>
121
+ </view>
122
+ </view>
123
+
124
+ <!-- 附件选择器弹窗 -->
125
+ <attachment-picker
126
+ wx:if="{{showAttachment}}"
127
+ visible="{{showAttachment}}"
128
+ maxCount="{{5}}"
129
+ bind:select="onAttachmentSelect"
130
+ bind:close="hideAttachmentPicker"
131
+ />
132
+ </view>
@@ -0,0 +1,324 @@
1
+ /* 输入栏容器 */
2
+ .input-bar-container {
3
+ background-color: #F3F3F3;
4
+ }
5
+
6
+ /* 附件预览区 */
7
+ .attachments-preview {
8
+ display: flex;
9
+ flex-wrap: wrap;
10
+ gap: 16rpx;
11
+ padding: 20rpx 30rpx 0;
12
+ }
13
+
14
+ .attachment-item {
15
+ position: relative;
16
+ width: 120rpx;
17
+ height: 120rpx;
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ .attachment-thumb {
22
+ width: 120rpx;
23
+ height: 120rpx;
24
+ border-radius: 12rpx;
25
+ background-color: #e0e0e0;
26
+ }
27
+
28
+ .attachment-file {
29
+ width: 100%;
30
+ height: 100%;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ justify-content: center;
35
+ background-color: #f5f5f5;
36
+ border-radius: 12rpx;
37
+ }
38
+
39
+ .attachment-file .file-icon {
40
+ font-size: 40rpx;
41
+ }
42
+
43
+ .attachment-file .file-name {
44
+ font-size: 20rpx;
45
+ color: #666;
46
+ margin-top: 8rpx;
47
+ max-width: 100rpx;
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .attachment-remove {
54
+ position: absolute;
55
+ top: -12rpx;
56
+ right: -12rpx;
57
+ width: 36rpx;
58
+ height: 36rpx;
59
+ background-color: rgba(0, 0, 0, 0.6);
60
+ border-radius: 50%;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ }
65
+
66
+ .attachment-remove text {
67
+ color: #fff;
68
+ font-size: 24rpx;
69
+ line-height: 1;
70
+ }
71
+
72
+ .attachment-progress {
73
+ position: absolute;
74
+ bottom: 0;
75
+ left: 0;
76
+ right: 0;
77
+ height: 6rpx;
78
+ background-color: rgba(0, 0, 0, 0.3);
79
+ border-radius: 0 0 12rpx 12rpx;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .progress-bar {
84
+ height: 100%;
85
+ background-color: #007aff;
86
+ transition: width 0.3s;
87
+ }
88
+
89
+ /* 输入区域 */
90
+ .input-area {
91
+ display: flex;
92
+ align-items: center;
93
+ padding: 16rpx 24rpx;
94
+ gap: 16rpx;
95
+ }
96
+
97
+ /* 模式切换按钮 */
98
+ .mode-switch {
99
+ width: 64rpx;
100
+ height: 64rpx;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ flex-shrink: 0;
105
+ }
106
+
107
+ .mode-icon {
108
+ width: 56rpx;
109
+ height: 56rpx;
110
+ }
111
+
112
+ /* 文本输入框 */
113
+ .text-input-wrapper {
114
+ flex: 1;
115
+ background-color: #fff;
116
+ border-radius: 16rpx;
117
+ border: 1rpx solid #E0E0E0;
118
+ padding: 0rpx 24rpx 0rpx 24rpx;
119
+ min-height: 82rpx;
120
+ box-sizing: border-box;
121
+ display: flex;
122
+ align-items: center;
123
+ }
124
+
125
+ .text-input {
126
+ width: 100%;
127
+ font-size: 30rpx;
128
+ line-height: 1.4;
129
+ min-height: 40rpx;
130
+ color: #333;
131
+ }
132
+
133
+ .text-input-placeholder {
134
+ color: #999;
135
+ }
136
+
137
+ /* 语音按钮 */
138
+ .voice-btn {
139
+ flex: 1;
140
+ height: 82rpx;
141
+ padding: 16rpx 24rpx;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ background-color: #fff;
146
+ border-radius: 16rpx;
147
+ border: 1rpx solid #E0E0E0;
148
+ font-size: 28rpx;
149
+ color: #333;
150
+ transition: all 0.2s;
151
+ box-sizing: border-box;
152
+ }
153
+
154
+ .voice-btn:active,
155
+ .voice-btn.recording {
156
+ background-color: #ffebee;
157
+ color: #f44336;
158
+ }
159
+
160
+ /* 语音取消区域样式 */
161
+ .voice-btn.cancel {
162
+ background-color: #ffcdd2;
163
+ color: #c62828;
164
+ border-color: #ef9a9a;
165
+ }
166
+
167
+ /* 附件按钮 */
168
+ .attachment-btn {
169
+ width: 64rpx;
170
+ height: 64rpx;
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ flex-shrink: 0;
175
+ }
176
+
177
+ /* 发送按钮 */
178
+ .send-btn {
179
+ width: 64rpx;
180
+ height: 64rpx;
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ background-color: #ccc;
185
+ border-radius: 50%;
186
+ flex-shrink: 0;
187
+ transition: background-color 0.2s;
188
+ }
189
+
190
+ .send-btn.active {
191
+ background-color: #007aff;
192
+ }
193
+
194
+ .send-btn.stop {
195
+ background-color: #ff4444;
196
+ }
197
+
198
+ .send-icon {
199
+ font-size: 32rpx;
200
+ color: #fff;
201
+ }
202
+
203
+ .stop-icon {
204
+ width: 24rpx;
205
+ height: 24rpx;
206
+ background-color: #fff;
207
+ border-radius: 4rpx;
208
+ }
209
+
210
+ /* 录音状态提示 */
211
+ .recording-overlay {
212
+ position: fixed;
213
+ top: 0;
214
+ left: 0;
215
+ right: 0;
216
+ bottom: 0;
217
+ background-color: rgba(0, 0, 0, 0.6);
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ z-index: 1000;
222
+ }
223
+
224
+ .recording-content {
225
+ display: flex;
226
+ flex-direction: column;
227
+ align-items: center;
228
+ padding: 60rpx;
229
+ background-color: rgba(0, 0, 0, 0.8);
230
+ border-radius: 24rpx;
231
+ }
232
+
233
+ .recording-animation {
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ gap: 12rpx;
238
+ height: 100rpx;
239
+ margin-bottom: 30rpx;
240
+ }
241
+
242
+ .wave {
243
+ width: 8rpx;
244
+ height: 60rpx;
245
+ background-color: #fff;
246
+ border-radius: 4rpx;
247
+ animation: wave 0.5s ease-in-out infinite;
248
+ }
249
+
250
+ .wave1 {
251
+ animation-delay: 0s;
252
+ }
253
+
254
+ .wave2 {
255
+ animation-delay: 0.1s;
256
+ }
257
+
258
+ .wave3 {
259
+ animation-delay: 0.2s;
260
+ }
261
+
262
+ @keyframes wave {
263
+ 0%, 100% {
264
+ height: 20rpx;
265
+ }
266
+ 50% {
267
+ height: 80rpx;
268
+ }
269
+ }
270
+
271
+ .recording-text {
272
+ font-size: 32rpx;
273
+ color: #fff;
274
+ margin-bottom: 16rpx;
275
+ }
276
+
277
+ .recording-tip {
278
+ font-size: 26rpx;
279
+ color: rgba(255, 255, 255, 0.6);
280
+ }
281
+
282
+ /* 识别中提示 */
283
+ .recognizing-overlay {
284
+ position: fixed;
285
+ top: 0;
286
+ left: 0;
287
+ right: 0;
288
+ bottom: 0;
289
+ background-color: rgba(0, 0, 0, 0.6);
290
+ display: flex;
291
+ align-items: center;
292
+ justify-content: center;
293
+ z-index: 1000;
294
+ }
295
+
296
+ .recognizing-content {
297
+ display: flex;
298
+ flex-direction: column;
299
+ align-items: center;
300
+ padding: 60rpx;
301
+ background-color: rgba(0, 0, 0, 0.8);
302
+ border-radius: 24rpx;
303
+ }
304
+
305
+ .recognizing-spinner {
306
+ width: 60rpx;
307
+ height: 60rpx;
308
+ border: 4rpx solid rgba(255, 255, 255, 0.3);
309
+ border-top-color: #fff;
310
+ border-radius: 50%;
311
+ animation: spin 0.8s linear infinite;
312
+ margin-bottom: 30rpx;
313
+ }
314
+
315
+ @keyframes spin {
316
+ to {
317
+ transform: rotate(360deg);
318
+ }
319
+ }
320
+
321
+ .recognizing-text {
322
+ font-size: 32rpx;
323
+ color: #fff;
324
+ }