@flexem/chat-box 1.0.9 → 1.0.11
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/miniprogram_dist/components/input-bar/index.js +39 -5
- package/miniprogram_dist/components/input-bar/index.wxml +1 -0
- package/miniprogram_dist/components/input-bar/index.wxss +14 -11
- package/miniprogram_dist/components/sidebar/index.js +86 -33
- package/miniprogram_dist/components/sidebar/index.wxml +28 -0
- package/miniprogram_dist/components/sidebar/index.wxss +75 -1
- package/miniprogram_dist/index.js +61 -15
- package/miniprogram_dist/index.wxml +8 -8
- package/miniprogram_dist/index.wxss +3 -2
- package/miniprogram_dist/package.json +1 -1
- package/miniprogram_dist/utils/audio.js +31 -0
- package/package.json +1 -1
|
@@ -80,7 +80,7 @@ Component({
|
|
|
80
80
|
data: {
|
|
81
81
|
inputValue: '', // 输入内容
|
|
82
82
|
inputHeight: 50, // 输入框高度
|
|
83
|
-
maxInputHeight:
|
|
83
|
+
maxInputHeight: 210, // 最大输入框高度(5行 × 42rpx)
|
|
84
84
|
isVoiceMode: false, // 是否语音模式
|
|
85
85
|
isRecording: false, // 是否正在录音
|
|
86
86
|
isRecognizing: false, // 是否正在识别
|
|
@@ -92,7 +92,8 @@ Component({
|
|
|
92
92
|
textareaFocus: false, // textarea 是否聚焦
|
|
93
93
|
recognizedText: '', // 语音识别的中间结果
|
|
94
94
|
recordStartTime: 0, // 录音开始时间
|
|
95
|
-
voiceStartY: 0
|
|
95
|
+
voiceStartY: 0, // 录音开始时的 Y 坐标
|
|
96
|
+
placeholderStyle: '' // placeholder 样式(Android 需要特殊处理垂直居中)
|
|
96
97
|
},
|
|
97
98
|
|
|
98
99
|
observers: {
|
|
@@ -125,6 +126,16 @@ Component({
|
|
|
125
126
|
if (this.properties.defaultValue) {
|
|
126
127
|
this.setData({ inputValue: this.properties.defaultValue });
|
|
127
128
|
}
|
|
129
|
+
|
|
130
|
+
// 检测平台,Android 需要特殊处理 textarea placeholder 垂直居中
|
|
131
|
+
const systemInfo = wx.getSystemInfoSync();
|
|
132
|
+
const isAndroid = systemInfo.platform === 'android';
|
|
133
|
+
if (isAndroid) {
|
|
134
|
+
// Android 上 textarea 的 placeholder 不会垂直居中,需要通过 padding-top 调整
|
|
135
|
+
this.setData({
|
|
136
|
+
placeholderStyle: 'padding-top: 15rpx;'
|
|
137
|
+
});
|
|
138
|
+
}
|
|
128
139
|
},
|
|
129
140
|
detached() {
|
|
130
141
|
if (this.data.isRecording) {
|
|
@@ -282,10 +293,11 @@ Component({
|
|
|
282
293
|
* 输入框行数变化
|
|
283
294
|
*/
|
|
284
295
|
onLineChange(e) {
|
|
285
|
-
const lineHeight =
|
|
286
|
-
const baseHeight = 50; //
|
|
296
|
+
const lineHeight = 42; // 基于 font-size: 30rpx, line-height: 1.4
|
|
297
|
+
const baseHeight = 50; // 单行基准高度(和语音按钮内容高度一致)
|
|
287
298
|
const lines = Math.min(e.detail.lineCount, 5);
|
|
288
|
-
|
|
299
|
+
// 单行时使用基准高度,多行时使用行高计算
|
|
300
|
+
const newHeight = lines <= 1 ? baseHeight : lineHeight * lines;
|
|
289
301
|
this.setData({
|
|
290
302
|
inputHeight: newHeight
|
|
291
303
|
});
|
|
@@ -341,6 +353,28 @@ Component({
|
|
|
341
353
|
return;
|
|
342
354
|
}
|
|
343
355
|
|
|
356
|
+
// 检查是否有正在上传的附件
|
|
357
|
+
const uploadingAttachments = this.data.attachments.filter(item => item.uploading);
|
|
358
|
+
if (uploadingAttachments.length > 0) {
|
|
359
|
+
const hasUploadingImage = uploadingAttachments.some(item => item.type === 'image');
|
|
360
|
+
const hasUploadingFile = uploadingAttachments.some(item => item.type === 'file');
|
|
361
|
+
|
|
362
|
+
let message = '';
|
|
363
|
+
if (hasUploadingImage && hasUploadingFile) {
|
|
364
|
+
message = '图片和文件上传中,请稍后';
|
|
365
|
+
} else if (hasUploadingImage) {
|
|
366
|
+
message = '图片上传中,请稍后';
|
|
367
|
+
} else {
|
|
368
|
+
message = '文件上传中,请稍后';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
wx.showToast({
|
|
372
|
+
title: message,
|
|
373
|
+
icon: 'none'
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
344
378
|
// 通知父组件停止语音播放
|
|
345
379
|
this.triggerEvent('voicerecordstart');
|
|
346
380
|
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
|
|
53
53
|
.attachment-remove {
|
|
54
54
|
position: absolute;
|
|
55
|
-
top: -
|
|
56
|
-
right: -
|
|
57
|
-
width:
|
|
58
|
-
height:
|
|
55
|
+
top: -14rpx;
|
|
56
|
+
right: -14rpx;
|
|
57
|
+
width: 40rpx;
|
|
58
|
+
height: 40rpx;
|
|
59
59
|
background-color: rgba(0, 0, 0, 0.6);
|
|
60
60
|
border-radius: 50%;
|
|
61
61
|
display: flex;
|
|
@@ -65,8 +65,9 @@
|
|
|
65
65
|
|
|
66
66
|
.attachment-remove text {
|
|
67
67
|
color: #fff;
|
|
68
|
-
font-size:
|
|
68
|
+
font-size: 32rpx;
|
|
69
69
|
line-height: 1;
|
|
70
|
+
margin-top: -6rpx;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
.attachment-progress {
|
|
@@ -115,19 +116,20 @@
|
|
|
115
116
|
background-color: #fff;
|
|
116
117
|
border-radius: 16rpx;
|
|
117
118
|
border: 1rpx solid #E0E0E0;
|
|
118
|
-
padding:
|
|
119
|
-
min-height:
|
|
119
|
+
padding: 0 24rpx;
|
|
120
|
+
min-height: 92rpx;
|
|
120
121
|
box-sizing: border-box;
|
|
121
122
|
display: flex;
|
|
122
123
|
align-items: center;
|
|
124
|
+
overflow: hidden;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
.text-input {
|
|
126
128
|
width: 100%;
|
|
127
129
|
font-size: 30rpx;
|
|
128
130
|
line-height: 1.4;
|
|
129
|
-
min-height: 40rpx;
|
|
130
131
|
color: #333;
|
|
132
|
+
min-height: 50rpx;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
.text-input-placeholder {
|
|
@@ -137,15 +139,16 @@
|
|
|
137
139
|
/* 语音按钮 */
|
|
138
140
|
.voice-btn {
|
|
139
141
|
flex: 1;
|
|
140
|
-
height:
|
|
141
|
-
padding:
|
|
142
|
+
height: 92rpx;
|
|
143
|
+
padding: 32rpx 24rpx;
|
|
142
144
|
display: flex;
|
|
143
145
|
align-items: center;
|
|
144
146
|
justify-content: center;
|
|
145
147
|
background-color: #fff;
|
|
146
148
|
border-radius: 16rpx;
|
|
147
149
|
border: 1rpx solid #E0E0E0;
|
|
148
|
-
font-size:
|
|
150
|
+
font-size: 30rpx;
|
|
151
|
+
font-weight: bold;
|
|
149
152
|
color: #333;
|
|
150
153
|
transition: all 0.2s;
|
|
151
154
|
box-sizing: border-box;
|
|
@@ -52,7 +52,11 @@ Component({
|
|
|
52
52
|
searchKeyword: '', // 搜索关键词
|
|
53
53
|
// 胶囊按钮位置
|
|
54
54
|
menuButtonTop: 0,
|
|
55
|
-
menuButtonHeight: 0
|
|
55
|
+
menuButtonHeight: 0,
|
|
56
|
+
// 重命名模态框
|
|
57
|
+
showRenameModal: false,
|
|
58
|
+
renameValue: '',
|
|
59
|
+
renamingSession: null
|
|
56
60
|
},
|
|
57
61
|
|
|
58
62
|
lifetimes: {
|
|
@@ -272,8 +276,13 @@ Component({
|
|
|
272
276
|
* 选择会话
|
|
273
277
|
*/
|
|
274
278
|
onSelectSession(e) {
|
|
275
|
-
if (this.data.isEditMode) return;
|
|
276
279
|
const session = e.currentTarget.dataset.session;
|
|
280
|
+
|
|
281
|
+
// 如果在编辑模式,先退出编辑模式
|
|
282
|
+
if (this.data.isEditMode) {
|
|
283
|
+
this.setData({ isEditMode: false });
|
|
284
|
+
}
|
|
285
|
+
|
|
277
286
|
this.triggerEvent('select', { session });
|
|
278
287
|
this.onClose();
|
|
279
288
|
},
|
|
@@ -331,45 +340,89 @@ Component({
|
|
|
331
340
|
},
|
|
332
341
|
|
|
333
342
|
/**
|
|
334
|
-
* 重命名对话
|
|
343
|
+
* 重命名对话 - 显示自定义模态框
|
|
335
344
|
*/
|
|
336
345
|
onRename(e) {
|
|
337
346
|
const session = e.currentTarget.dataset.session;
|
|
347
|
+
this.setData({
|
|
348
|
+
showRenameModal: true,
|
|
349
|
+
renameValue: session.title || '',
|
|
350
|
+
renamingSession: session
|
|
351
|
+
});
|
|
352
|
+
},
|
|
338
353
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
await api.renameChatSession(
|
|
348
|
-
this.properties.baseUrl,
|
|
349
|
-
this.properties.token,
|
|
350
|
-
session.id,
|
|
351
|
-
res.content.trim()
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
// 更新本地数据
|
|
355
|
-
this.updateSessionTitle(session.id, res.content.trim());
|
|
354
|
+
/**
|
|
355
|
+
* 重命名输入变化
|
|
356
|
+
*/
|
|
357
|
+
onRenameInput(e) {
|
|
358
|
+
this.setData({
|
|
359
|
+
renameValue: e.detail.value
|
|
360
|
+
});
|
|
361
|
+
},
|
|
356
362
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
icon: 'none'
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
363
|
+
/**
|
|
364
|
+
* 取消重命名
|
|
365
|
+
*/
|
|
366
|
+
onRenameCancel() {
|
|
367
|
+
this.setData({
|
|
368
|
+
showRenameModal: false,
|
|
369
|
+
renameValue: '',
|
|
370
|
+
renamingSession: null
|
|
370
371
|
});
|
|
371
372
|
},
|
|
372
373
|
|
|
374
|
+
/**
|
|
375
|
+
* 确认重命名
|
|
376
|
+
*/
|
|
377
|
+
async onRenameConfirm() {
|
|
378
|
+
const { renameValue, renamingSession } = this.data;
|
|
379
|
+
|
|
380
|
+
if (!renameValue || !renameValue.trim()) {
|
|
381
|
+
wx.showToast({
|
|
382
|
+
title: '请输入标题',
|
|
383
|
+
icon: 'none'
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
await api.renameChatSession(
|
|
390
|
+
this.properties.baseUrl,
|
|
391
|
+
this.properties.token,
|
|
392
|
+
renamingSession.id,
|
|
393
|
+
renameValue.trim()
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// 更新本地数据
|
|
397
|
+
this.updateSessionTitle(renamingSession.id, renameValue.trim());
|
|
398
|
+
|
|
399
|
+
// 关闭模态框
|
|
400
|
+
this.setData({
|
|
401
|
+
showRenameModal: false,
|
|
402
|
+
renameValue: '',
|
|
403
|
+
renamingSession: null
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
wx.showToast({
|
|
407
|
+
title: '重命名成功',
|
|
408
|
+
icon: 'success'
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error('重命名失败', error);
|
|
412
|
+
wx.showToast({
|
|
413
|
+
title: error.message || '重命名失败',
|
|
414
|
+
icon: 'none'
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 阻止模态框点击冒泡
|
|
421
|
+
*/
|
|
422
|
+
preventModalBubble() {
|
|
423
|
+
// 空函数,阻止事件冒泡
|
|
424
|
+
},
|
|
425
|
+
|
|
373
426
|
/**
|
|
374
427
|
* 更新会话标题
|
|
375
428
|
*/
|
|
@@ -134,4 +134,32 @@
|
|
|
134
134
|
</view>
|
|
135
135
|
</view>
|
|
136
136
|
</view>
|
|
137
|
+
|
|
138
|
+
<!-- 重命名模态框 -->
|
|
139
|
+
<view wx:if="{{showRenameModal}}" class="rename-modal-overlay" catchtap="onRenameCancel">
|
|
140
|
+
<view class="rename-modal" catchtap="preventModalBubble">
|
|
141
|
+
<view class="rename-modal-header">
|
|
142
|
+
<text class="rename-modal-title">重命名</text>
|
|
143
|
+
</view>
|
|
144
|
+
<view class="rename-modal-body">
|
|
145
|
+
<input
|
|
146
|
+
class="rename-input"
|
|
147
|
+
value="{{renameValue}}"
|
|
148
|
+
placeholder="请输入新标题"
|
|
149
|
+
focus="{{showRenameModal}}"
|
|
150
|
+
bindinput="onRenameInput"
|
|
151
|
+
bindconfirm="onRenameConfirm"
|
|
152
|
+
maxlength="50"
|
|
153
|
+
/>
|
|
154
|
+
</view>
|
|
155
|
+
<view class="rename-modal-footer">
|
|
156
|
+
<view class="rename-modal-btn rename-cancel-btn" bindtap="onRenameCancel">
|
|
157
|
+
<text>取消</text>
|
|
158
|
+
</view>
|
|
159
|
+
<view class="rename-modal-btn rename-confirm-btn" bindtap="onRenameConfirm">
|
|
160
|
+
<text>确定</text>
|
|
161
|
+
</view>
|
|
162
|
+
</view>
|
|
163
|
+
</view>
|
|
164
|
+
</view>
|
|
137
165
|
</view>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
left: 0;
|
|
6
6
|
width: 85vw;
|
|
7
7
|
height: 100%;
|
|
8
|
-
z-index:
|
|
8
|
+
z-index: 20;
|
|
9
9
|
pointer-events: none;
|
|
10
10
|
opacity: 0;
|
|
11
11
|
transition: opacity 0.3s ease;
|
|
@@ -260,3 +260,77 @@
|
|
|
260
260
|
color: #333;
|
|
261
261
|
font-weight: bold;
|
|
262
262
|
}
|
|
263
|
+
|
|
264
|
+
/* 重命名模态框 */
|
|
265
|
+
.rename-modal-overlay {
|
|
266
|
+
position: fixed;
|
|
267
|
+
top: 0;
|
|
268
|
+
left: 0;
|
|
269
|
+
right: 0;
|
|
270
|
+
bottom: 0;
|
|
271
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
justify-content: center;
|
|
275
|
+
z-index: 9999;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.rename-modal {
|
|
279
|
+
width: 80%;
|
|
280
|
+
max-width: 560rpx;
|
|
281
|
+
background-color: #fff;
|
|
282
|
+
border-radius: 24rpx;
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.rename-modal-header {
|
|
287
|
+
padding: 32rpx;
|
|
288
|
+
text-align: center;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.rename-modal-title {
|
|
292
|
+
font-size: 34rpx;
|
|
293
|
+
font-weight: 500;
|
|
294
|
+
color: #333;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.rename-modal-body {
|
|
298
|
+
padding: 0 32rpx 32rpx;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.rename-input {
|
|
302
|
+
width: 100%;
|
|
303
|
+
height: 80rpx;
|
|
304
|
+
padding: 0 24rpx;
|
|
305
|
+
background-color: #f5f5f5;
|
|
306
|
+
border-radius: 12rpx;
|
|
307
|
+
font-size: 30rpx;
|
|
308
|
+
box-sizing: border-box;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.rename-modal-footer {
|
|
312
|
+
display: flex;
|
|
313
|
+
border-top: 1rpx solid #eee;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.rename-modal-btn {
|
|
317
|
+
flex: 1;
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
justify-content: center;
|
|
321
|
+
padding: 28rpx;
|
|
322
|
+
font-size: 32rpx;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.rename-modal-btn:active {
|
|
326
|
+
background-color: #f5f5f5;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.rename-cancel-btn {
|
|
330
|
+
color: #666;
|
|
331
|
+
border-right: 1rpx solid #eee;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.rename-confirm-btn {
|
|
335
|
+
color: #007aff;
|
|
336
|
+
}
|
|
@@ -399,6 +399,12 @@ Component({
|
|
|
399
399
|
return;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
// 停止语音播放
|
|
403
|
+
if (this.data.playingMessageId) {
|
|
404
|
+
audio.stop();
|
|
405
|
+
this.setData({ playingMessageId: null, synthesizingMessageId: null });
|
|
406
|
+
}
|
|
407
|
+
|
|
402
408
|
this.setData({ showSidebar: true });
|
|
403
409
|
// 刷新对话列表
|
|
404
410
|
const sidebar = this.selectComponent('#sidebar');
|
|
@@ -761,11 +767,24 @@ Component({
|
|
|
761
767
|
};
|
|
762
768
|
|
|
763
769
|
const messages = [...this.data.messages, userMessage];
|
|
764
|
-
this.setData({ messages })
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
770
|
+
this.setData({ messages }, () => {
|
|
771
|
+
// 使用 nextTick 确保 DOM 已更新
|
|
772
|
+
wx.nextTick(() => {
|
|
773
|
+
// 如果有图片附件,延迟一下让图片开始加载
|
|
774
|
+
const hasImages = attachments && attachments.some(att => att.type === 'image');
|
|
775
|
+
if (hasImages) {
|
|
776
|
+
setTimeout(() => {
|
|
777
|
+
this.scrollToBottom(true);
|
|
778
|
+
// 发送请求
|
|
779
|
+
this.sendChatRequest(content, attachments, isVoice);
|
|
780
|
+
}, 150);
|
|
781
|
+
} else {
|
|
782
|
+
this.scrollToBottom();
|
|
783
|
+
// 发送请求
|
|
784
|
+
this.sendChatRequest(content, attachments, isVoice);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
});
|
|
769
788
|
},
|
|
770
789
|
|
|
771
790
|
/**
|
|
@@ -1007,7 +1026,10 @@ Component({
|
|
|
1007
1026
|
streamingMessage: null,
|
|
1008
1027
|
isStreaming: false,
|
|
1009
1028
|
playingMessageId: null,
|
|
1010
|
-
synthesizingMessageId: null
|
|
1029
|
+
synthesizingMessageId: null
|
|
1030
|
+
}, () => {
|
|
1031
|
+
// 停止生成后滚动到底部
|
|
1032
|
+
this.scrollToBottom(true);
|
|
1011
1033
|
});
|
|
1012
1034
|
} else {
|
|
1013
1035
|
this.setData({
|
|
@@ -1091,10 +1113,16 @@ Component({
|
|
|
1091
1113
|
isVoice: message.isVoice
|
|
1092
1114
|
};
|
|
1093
1115
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1116
|
+
// 重置用户滚动状态,确保重新生成时能自动滚动
|
|
1117
|
+
this.setData({
|
|
1118
|
+
messages: [...messages, userMessage],
|
|
1119
|
+
userScrolledUp: false
|
|
1120
|
+
}, () => {
|
|
1121
|
+
// 先滚动到底部,再发送请求
|
|
1122
|
+
this.scrollToBottom(true);
|
|
1123
|
+
// 重新发送该用户消息
|
|
1124
|
+
this.sendChatRequest(message.content, message.attachments, message.isVoice);
|
|
1125
|
+
});
|
|
1098
1126
|
},
|
|
1099
1127
|
|
|
1100
1128
|
/**
|
|
@@ -1401,7 +1429,7 @@ Component({
|
|
|
1401
1429
|
*/
|
|
1402
1430
|
scrollToBottom(force = false) {
|
|
1403
1431
|
const now = Date.now();
|
|
1404
|
-
const throttleInterval =
|
|
1432
|
+
const throttleInterval = 100; // 100ms 节流(更快响应)
|
|
1405
1433
|
|
|
1406
1434
|
// 流式输出时,如果用户手动向上滚动了,不自动滚动(除非强制)
|
|
1407
1435
|
if (!force && this.data.isStreaming && this.data.userScrolledUp) {
|
|
@@ -1422,12 +1450,25 @@ Component({
|
|
|
1422
1450
|
}
|
|
1423
1451
|
}
|
|
1424
1452
|
|
|
1425
|
-
//
|
|
1426
|
-
|
|
1453
|
+
// 设置自动滚动标志,防止 onScroll 误判为用户手动滚动
|
|
1454
|
+
this.isAutoScrolling = true;
|
|
1455
|
+
|
|
1456
|
+
// 先清空 scrollToMessage,再设置为目标值,确保触发滚动
|
|
1457
|
+
// 因为 scroll-into-view 值不变时不会重新触发滚动
|
|
1427
1458
|
this.setData({
|
|
1428
|
-
scrollToMessage: '
|
|
1429
|
-
scrollCounter: counter,
|
|
1459
|
+
scrollToMessage: '',
|
|
1430
1460
|
lastScrollTime: now
|
|
1461
|
+
}, () => {
|
|
1462
|
+
// 在下一个 tick 设置目标值
|
|
1463
|
+
wx.nextTick(() => {
|
|
1464
|
+
this.setData({
|
|
1465
|
+
scrollToMessage: 'scroll-bottom'
|
|
1466
|
+
});
|
|
1467
|
+
// 延迟清除自动滚动标志,等待滚动动画完成
|
|
1468
|
+
setTimeout(() => {
|
|
1469
|
+
this.isAutoScrolling = false;
|
|
1470
|
+
}, 300);
|
|
1471
|
+
});
|
|
1431
1472
|
});
|
|
1432
1473
|
},
|
|
1433
1474
|
|
|
@@ -1450,6 +1491,11 @@ Component({
|
|
|
1450
1491
|
return;
|
|
1451
1492
|
}
|
|
1452
1493
|
|
|
1494
|
+
// 如果是自动滚动触发的,忽略
|
|
1495
|
+
if (this.isAutoScrolling) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1453
1499
|
const { deltaY } = e.detail;
|
|
1454
1500
|
|
|
1455
1501
|
// deltaY > 0 表示向上滚动(手指向下滑)
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
<!-- 消息列表区域 -->
|
|
88
88
|
<scroll-view
|
|
89
89
|
class="message-list"
|
|
90
|
-
style="margin-top: {{navBarHeight}}px; padding-bottom: {{keyboardHeight > 0 ? (keyboardHeight + inputBarHeight +
|
|
90
|
+
style="margin-top: {{navBarHeight}}px; padding-bottom: {{keyboardHeight > 0 ? (keyboardHeight + inputBarHeight + 62) : (83 + safeAreaBottom + attachmentHeight + inputBarHeight)}}px;"
|
|
91
91
|
scroll-y="true"
|
|
92
92
|
scroll-into-view="{{scrollToMessage}}"
|
|
93
93
|
enhanced="true"
|
|
@@ -104,6 +104,13 @@
|
|
|
104
104
|
|
|
105
105
|
<!-- 消息列表 -->
|
|
106
106
|
<view class="message-wrapper">
|
|
107
|
+
<!-- 空状态 -->
|
|
108
|
+
<view wx:if="{{messages.length === 0 && !streamingMessage}}" class="empty-state">
|
|
109
|
+
<view class="empty-icon"></view>
|
|
110
|
+
<text class="empty-text">开始新对话</text>
|
|
111
|
+
<text class="empty-hint">输入问题或按住说话</text>
|
|
112
|
+
</view>
|
|
113
|
+
|
|
107
114
|
<block wx:for="{{messages}}" wx:key="id">
|
|
108
115
|
<message-item
|
|
109
116
|
id="msg-{{item.id}}"
|
|
@@ -136,13 +143,6 @@
|
|
|
136
143
|
/>
|
|
137
144
|
</view>
|
|
138
145
|
|
|
139
|
-
<!-- 空状态 -->
|
|
140
|
-
<view wx:if="{{messages.length === 0 && !streamingMessage}}" class="empty-state">
|
|
141
|
-
<view class="empty-icon"></view>
|
|
142
|
-
<text class="empty-text">开始新对话</text>
|
|
143
|
-
<text class="empty-hint">输入问题或按住说话</text>
|
|
144
|
-
</view>
|
|
145
|
-
|
|
146
146
|
<!-- 底部占位 -->
|
|
147
147
|
<view class="scroll-bottom-spacer" id="scroll-bottom"></view>
|
|
148
148
|
</scroll-view>
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
.message-wrapper {
|
|
221
|
-
padding-bottom:
|
|
221
|
+
padding-bottom: 10rpx;
|
|
222
222
|
/* 设置最小高度为100%+额外空间,确保能够滚动 */
|
|
223
223
|
min-height: calc(100%);
|
|
224
224
|
}
|
|
@@ -241,6 +241,7 @@
|
|
|
241
241
|
flex-direction: column;
|
|
242
242
|
align-items: center;
|
|
243
243
|
justify-content: center;
|
|
244
|
+
min-height: 100%; /* 占满整个 message-wrapper */
|
|
244
245
|
padding: 100rpx 0;
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -262,7 +263,7 @@
|
|
|
262
263
|
|
|
263
264
|
/* 底部占位 */
|
|
264
265
|
.scroll-bottom-spacer {
|
|
265
|
-
height:
|
|
266
|
+
height: 0rpx;
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
/* 底部区域 */
|
|
@@ -11,6 +11,7 @@ let currentPlayingId = null;
|
|
|
11
11
|
let onPlayStateChange = null;
|
|
12
12
|
let currentSegmentPlayer = null; // 当前正在播放的片段播放器(用于分段播放)
|
|
13
13
|
let currentPlayState = null; // 当前的 playState 对象引用(用于停止时标记)
|
|
14
|
+
let pendingStopCallback = false; // 是否有待处理的 stop 回调(用于区分 onEnded 是 stop 触发还是正常播放完毕)
|
|
14
15
|
|
|
15
16
|
// 流式播放队列相关
|
|
16
17
|
let streamingId = null; // 当前流式播放的消息 ID
|
|
@@ -55,6 +56,10 @@ function initAudioPlayer() {
|
|
|
55
56
|
|
|
56
57
|
innerAudioContext.onStop(() => {
|
|
57
58
|
console.log('音频停止');
|
|
59
|
+
// 清除待处理的 stop 回调标志
|
|
60
|
+
pendingStopCallback = false;
|
|
61
|
+
// 如果 playText 正在用 segmentPlayer 管理播放,不要干扰
|
|
62
|
+
if (currentPlayState) return;
|
|
58
63
|
if (onPlayStateChange) {
|
|
59
64
|
onPlayStateChange({ playing: false, id: currentPlayingId });
|
|
60
65
|
}
|
|
@@ -64,11 +69,31 @@ function initAudioPlayer() {
|
|
|
64
69
|
|
|
65
70
|
innerAudioContext.onEnded(() => {
|
|
66
71
|
console.log('音频播放结束');
|
|
72
|
+
|
|
73
|
+
// 如果这个 onEnded 是由 stop() 触发的(而非正常播放完毕),不要进入流式播放分支
|
|
74
|
+
// 否则会错误地将其当作新流式播放的"音频段播放完毕",导致状态混乱
|
|
75
|
+
if (pendingStopCallback) {
|
|
76
|
+
console.log('onEnded 由 stop() 触发,忽略流式播放处理');
|
|
77
|
+
pendingStopCallback = false;
|
|
78
|
+
// 按停止逻辑处理
|
|
79
|
+
if (!currentPlayState) {
|
|
80
|
+
if (onPlayStateChange) {
|
|
81
|
+
onPlayStateChange({ playing: false, id: currentPlayingId });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
currentPlayingId = null;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
// 流式模式下,播放完当前段后继续播放队列中的下一段
|
|
68
89
|
if (isStreamingMode && streamingId) {
|
|
69
90
|
isPlayingAudio = false;
|
|
70
91
|
playNextAudio();
|
|
71
92
|
} else {
|
|
93
|
+
// 如果 playText 正在用 segmentPlayer 管理播放,不要干扰
|
|
94
|
+
// 否则 innerAudioContext.stop() 触发的异步 onEnded 会把 currentPlayingId 置空,
|
|
95
|
+
// 导致 tryPlayNext 的 currentPlayingId !== playState.id 判断失败,音频不播放
|
|
96
|
+
if (currentPlayState) return;
|
|
72
97
|
if (onPlayStateChange) {
|
|
73
98
|
onPlayStateChange({ playing: false, id: currentPlayingId });
|
|
74
99
|
}
|
|
@@ -155,6 +180,8 @@ function stop() {
|
|
|
155
180
|
|
|
156
181
|
// 停止全局播放器
|
|
157
182
|
if (innerAudioContext) {
|
|
183
|
+
// 标记有待处理的 stop 回调,防止异步 onEnded 被错误地当作新流式播放的"音频段播放完毕"
|
|
184
|
+
pendingStopCallback = true;
|
|
158
185
|
innerAudioContext.stop();
|
|
159
186
|
}
|
|
160
187
|
currentPlayingId = null;
|
|
@@ -622,6 +649,9 @@ function startStreamingPlay(id) {
|
|
|
622
649
|
stopStreamingPlay();
|
|
623
650
|
}
|
|
624
651
|
|
|
652
|
+
// 清除待处理的 stop 回调标志,确保新的流式播放不受之前 stop() 的影响
|
|
653
|
+
pendingStopCallback = false;
|
|
654
|
+
|
|
625
655
|
streamingId = id;
|
|
626
656
|
textQueue = [];
|
|
627
657
|
audioQueue = [];
|
|
@@ -816,6 +846,7 @@ function playNextAudio() {
|
|
|
816
846
|
console.log('流式播放全部完成');
|
|
817
847
|
const id = streamingId;
|
|
818
848
|
stopStreamingPlay();
|
|
849
|
+
currentPlayingId = null;
|
|
819
850
|
if (onPlayStateChange) {
|
|
820
851
|
onPlayStateChange({ playing: false, id: id });
|
|
821
852
|
}
|