@flexem/chat-box 1.0.2 → 1.0.4
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 +5 -1
- package/miniprogram_dist/components/input-bar/index.js +3 -0
- package/miniprogram_dist/components/message/index.js +166 -3
- package/miniprogram_dist/components/message/index.wxml +28 -8
- package/miniprogram_dist/components/message/index.wxss +101 -10
- package/miniprogram_dist/components/sidebar/index.js +32 -1
- package/miniprogram_dist/components/sidebar/index.wxml +2 -2
- package/miniprogram_dist/components/sidebar/index.wxss +3 -5
- package/miniprogram_dist/index.js +143 -3
- package/miniprogram_dist/index.wxml +26 -1
- package/miniprogram_dist/index.wxss +83 -1
- package/miniprogram_dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,7 +74,10 @@ npm run build
|
|
|
74
74
|
**request 合法域名:**
|
|
75
75
|
- `https://ids-dev.platform.flexem.net`(IDS 服务)
|
|
76
76
|
- `https://chatweb-dev.platform.flexem.net`(AI 聊天服务)
|
|
77
|
-
- `https://
|
|
77
|
+
- `https://chatbg.fbox360.com`(线上AI 聊天服务)
|
|
78
|
+
- `https://usso.fbox360.com`(线上IDS 服务)
|
|
79
|
+
- `https://oss-dev.platform.flexem.net`(线上OSS 服务)
|
|
80
|
+
- `https://os.fbox360.com`(线上OSS 服务)
|
|
78
81
|
- `https://up-z2.qiniup.com`(七牛云上传)
|
|
79
82
|
|
|
80
83
|
**uploadFile 合法域名:**
|
|
@@ -82,6 +85,7 @@ npm run build
|
|
|
82
85
|
|
|
83
86
|
**downloadFile 合法域名:**
|
|
84
87
|
- `https://blobstorage-dev.platform.flexem.net`(文件下载服务)
|
|
88
|
+
- `https://fuf.flexem.net`(线上文件下载服务)
|
|
85
89
|
|
|
86
90
|
### 2. 页面配置
|
|
87
91
|
|
|
@@ -42,7 +42,10 @@ Component({
|
|
|
42
42
|
showThinking: false, // 是否显示思考过程
|
|
43
43
|
lastParseTime: 0, // 上次解析时间
|
|
44
44
|
pendingParse: false, // 是否有待处理的解析
|
|
45
|
-
elementIdCounter: 0
|
|
45
|
+
elementIdCounter: 0, // 元素 ID 计数器,用于生成唯一 ID
|
|
46
|
+
// 长按气泡菜单
|
|
47
|
+
showBubbleMenu: false,
|
|
48
|
+
bubbleMenuPosition: { top: 0, left: 0 }
|
|
46
49
|
},
|
|
47
50
|
|
|
48
51
|
observers: {
|
|
@@ -634,7 +637,13 @@ Component({
|
|
|
634
637
|
const endPos = remaining.indexOf(marker, nearest.pos + 2);
|
|
635
638
|
if (endPos !== -1) {
|
|
636
639
|
const boldText = remaining.slice(nearest.pos + 2, endPos);
|
|
637
|
-
|
|
640
|
+
// 递归解析粗体内容,支持内部的链接等
|
|
641
|
+
const boldParts = this.processInlineElements(boldText);
|
|
642
|
+
// 给所有解析出的部分添加 bold 标记
|
|
643
|
+
boldParts.forEach(part => {
|
|
644
|
+
part.bold = true;
|
|
645
|
+
parts.push(part);
|
|
646
|
+
});
|
|
638
647
|
remaining = remaining.slice(endPos + 2);
|
|
639
648
|
} else {
|
|
640
649
|
// 没有闭合,当作普通文本
|
|
@@ -657,7 +666,13 @@ Component({
|
|
|
657
666
|
}
|
|
658
667
|
if (endPos !== -1) {
|
|
659
668
|
const italicText = remaining.slice(nearest.pos + 1, endPos);
|
|
660
|
-
|
|
669
|
+
// 递归解析斜体内容,支持内部的链接等
|
|
670
|
+
const italicParts = this.processInlineElements(italicText);
|
|
671
|
+
// 给所有解析出的部分添加 italic 标记
|
|
672
|
+
italicParts.forEach(part => {
|
|
673
|
+
part.italic = true;
|
|
674
|
+
parts.push(part);
|
|
675
|
+
});
|
|
661
676
|
remaining = remaining.slice(endPos + 1);
|
|
662
677
|
} else {
|
|
663
678
|
// 没有闭合,当作普通文本
|
|
@@ -961,6 +976,13 @@ Component({
|
|
|
961
976
|
this.triggerEvent('playvoice', { id, content: message.content });
|
|
962
977
|
},
|
|
963
978
|
|
|
979
|
+
/**
|
|
980
|
+
* 停止语音播放
|
|
981
|
+
*/
|
|
982
|
+
onStopVoice() {
|
|
983
|
+
this.triggerEvent('stopvoice');
|
|
984
|
+
},
|
|
985
|
+
|
|
964
986
|
/**
|
|
965
987
|
* 重新生成
|
|
966
988
|
*/
|
|
@@ -983,6 +1005,147 @@ Component({
|
|
|
983
1005
|
if (option) {
|
|
984
1006
|
this.triggerEvent('quickreply', { content: option });
|
|
985
1007
|
}
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
// ==================== 长按气泡菜单 ====================
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* 长按消息显示气泡菜单
|
|
1014
|
+
*/
|
|
1015
|
+
onLongPress(e) {
|
|
1016
|
+
// 只对 AI 消息显示气泡菜单
|
|
1017
|
+
if (this.properties.message.role !== 'assistant') {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// 获取触摸位置
|
|
1022
|
+
const touch = e.touches[0] || e.changedTouches[0];
|
|
1023
|
+
if (!touch) return;
|
|
1024
|
+
|
|
1025
|
+
// 获取屏幕宽度
|
|
1026
|
+
const systemInfo = wx.getWindowInfo();
|
|
1027
|
+
const screenWidth = systemInfo.windowWidth;
|
|
1028
|
+
|
|
1029
|
+
// 气泡菜单实际宽度:3个按钮(min-width 120rpx + padding 48rpx) + 外层 padding 16rpx = 约 520rpx
|
|
1030
|
+
// rpx 转 px: px = rpx / 750 * screenWidth
|
|
1031
|
+
const menuWidth = 520 / 750 * screenWidth;
|
|
1032
|
+
const menuHeight = 120;
|
|
1033
|
+
|
|
1034
|
+
// 计算气泡菜单位置(在触摸点上方,水平居中但不超出屏幕)
|
|
1035
|
+
const top = touch.clientY - menuHeight - 20;
|
|
1036
|
+
let left = touch.clientX;
|
|
1037
|
+
|
|
1038
|
+
// 确保菜单不超出左右边界(菜单使用 translateX(-50%))
|
|
1039
|
+
const halfMenu = menuWidth / 2;
|
|
1040
|
+
const minLeft = halfMenu + 16;
|
|
1041
|
+
const maxLeft = screenWidth - halfMenu - 16;
|
|
1042
|
+
|
|
1043
|
+
// 限制 left 在有效范围内
|
|
1044
|
+
left = Math.max(minLeft, Math.min(maxLeft, left));
|
|
1045
|
+
|
|
1046
|
+
this.setData({
|
|
1047
|
+
showBubbleMenu: true,
|
|
1048
|
+
bubbleMenuPosition: {
|
|
1049
|
+
top: Math.max(top, 50), // 确保不超出顶部
|
|
1050
|
+
left: left
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// 震动反馈
|
|
1055
|
+
wx.vibrateShort({ type: 'medium' });
|
|
1056
|
+
},
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* 隐藏气泡菜单
|
|
1060
|
+
*/
|
|
1061
|
+
hideBubbleMenu() {
|
|
1062
|
+
this.setData({ showBubbleMenu: false });
|
|
1063
|
+
},
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* 阻止事件冒泡
|
|
1067
|
+
*/
|
|
1068
|
+
stopPropagation() {
|
|
1069
|
+
// 空函数,用于阻止事件冒泡
|
|
1070
|
+
},
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* 复制全文
|
|
1074
|
+
*/
|
|
1075
|
+
onCopyAll() {
|
|
1076
|
+
const content = this.properties.message.content;
|
|
1077
|
+
wx.setClipboardData({
|
|
1078
|
+
data: content,
|
|
1079
|
+
success: () => {
|
|
1080
|
+
wx.showToast({
|
|
1081
|
+
title: '内容已复制',
|
|
1082
|
+
icon: 'success'
|
|
1083
|
+
});
|
|
1084
|
+
this.hideBubbleMenu();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* 节选复制 - 触发事件让父组件显示弹框
|
|
1091
|
+
*/
|
|
1092
|
+
onSelectCopy() {
|
|
1093
|
+
const content = this.properties.message.content;
|
|
1094
|
+
// 按段落分割内容
|
|
1095
|
+
const segments = this.splitContentToSegments(content);
|
|
1096
|
+
|
|
1097
|
+
this.setData({
|
|
1098
|
+
showBubbleMenu: false
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
// 触发事件,让父组件显示弹框
|
|
1102
|
+
this.triggerEvent('selectcopy', { segments });
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* 将内容按段落分割
|
|
1107
|
+
*/
|
|
1108
|
+
splitContentToSegments(content) {
|
|
1109
|
+
if (!content) return [];
|
|
1110
|
+
|
|
1111
|
+
// 按换行符分割,过滤空行,合并短段落
|
|
1112
|
+
const lines = content.split('\n');
|
|
1113
|
+
const segments = [];
|
|
1114
|
+
let currentSegment = '';
|
|
1115
|
+
|
|
1116
|
+
for (const line of lines) {
|
|
1117
|
+
const trimmedLine = line.trim();
|
|
1118
|
+
if (!trimmedLine) {
|
|
1119
|
+
// 空行,保存当前段落
|
|
1120
|
+
if (currentSegment) {
|
|
1121
|
+
segments.push({ text: currentSegment.trim() });
|
|
1122
|
+
currentSegment = '';
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
// 非空行
|
|
1126
|
+
if (currentSegment) {
|
|
1127
|
+
currentSegment += '\n' + trimmedLine;
|
|
1128
|
+
} else {
|
|
1129
|
+
currentSegment = trimmedLine;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// 保存最后一个段落
|
|
1135
|
+
if (currentSegment) {
|
|
1136
|
+
segments.push({ text: currentSegment.trim() });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return segments;
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* 气泡菜单 - 语音播放
|
|
1144
|
+
*/
|
|
1145
|
+
onBubblePlayVoice() {
|
|
1146
|
+
this.hideBubbleMenu();
|
|
1147
|
+
const message = this.properties.message;
|
|
1148
|
+
this.triggerEvent('playvoice', { id: message.id, content: message.content });
|
|
986
1149
|
}
|
|
987
1150
|
}
|
|
988
1151
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- 消息内容区域 -->
|
|
3
3
|
<view class="message-content">
|
|
4
4
|
<!-- 用户消息 -->
|
|
5
|
-
<view wx:if="{{message.role === 'user'}}" class="message-bubble user-bubble">
|
|
5
|
+
<view wx:if="{{message.role === 'user'}}" class="message-bubble user-bubble" bindlongpress="onLongPress">
|
|
6
6
|
<text class="message-text">{{message.content}}</text>
|
|
7
7
|
|
|
8
8
|
<!-- 附件预览 -->
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
</view>
|
|
40
40
|
|
|
41
41
|
<!-- AI 消息 -->
|
|
42
|
-
<view wx:else class="message-bubble ai-bubble {{message.isError ? 'error-bubble' : ''}}">
|
|
42
|
+
<view wx:else class="message-bubble ai-bubble {{message.isError ? 'error-bubble' : ''}}" bindlongpress="onLongPress">
|
|
43
43
|
<!-- 思考过程(可折叠) -->
|
|
44
44
|
<view wx:if="{{message.thinkingText}}" class="thinking-section">
|
|
45
45
|
<view class="thinking-header" bindtap="toggleThinking">
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
<text class="code-lang">{{item.lang || 'code'}}</text>
|
|
84
84
|
<text class="code-copy" bindtap="onCopyCode" data-code="{{item.text}}">复制</text>
|
|
85
85
|
</view>
|
|
86
|
-
<!-- 使用
|
|
87
|
-
<view class="code-content-wrapper">
|
|
86
|
+
<!-- 使用 scroll-view 实现横向滚动 -->
|
|
87
|
+
<scroll-view class="code-content-wrapper" scroll-x="true" enhanced="true" show-scrollbar="true">
|
|
88
88
|
<text class="code-text" user-select="{{true}}">{{item.text}}</text>
|
|
89
|
-
</view>
|
|
89
|
+
</scroll-view>
|
|
90
90
|
</view>
|
|
91
91
|
|
|
92
92
|
<!-- 行内代码 -->
|
|
@@ -243,14 +243,34 @@
|
|
|
243
243
|
</block>
|
|
244
244
|
</view>
|
|
245
245
|
|
|
246
|
+
<!-- 长按气泡菜单 -->
|
|
247
|
+
<view wx:if="{{showBubbleMenu}}" class="bubble-menu-overlay" catchtap="hideBubbleMenu">
|
|
248
|
+
<view class="bubble-menu" catchtap="stopPropagation" style="top: {{bubbleMenuPosition.top}}px; left: {{bubbleMenuPosition.left}}px;">
|
|
249
|
+
<view class="bubble-menu-item" bindtap="onCopyAll">
|
|
250
|
+
<image class="bubble-menu-icon" src="../../assets/icons/icon-copy.svg" mode="aspectFit" />
|
|
251
|
+
<text class="bubble-menu-text">复制全文</text>
|
|
252
|
+
</view>
|
|
253
|
+
<view class="bubble-menu-item" bindtap="onSelectCopy">
|
|
254
|
+
<text class="bubble-menu-icon-text">[A]</text>
|
|
255
|
+
<text class="bubble-menu-text">节选复制</text>
|
|
256
|
+
</view>
|
|
257
|
+
<view class="bubble-menu-item" bindtap="onBubblePlayVoice">
|
|
258
|
+
<image class="bubble-menu-icon" src="../../assets/icons/icon-play-voice.svg" mode="aspectFit" />
|
|
259
|
+
<text class="bubble-menu-text">语音播放</text>
|
|
260
|
+
</view>
|
|
261
|
+
<!-- 气泡箭头 -->
|
|
262
|
+
<view class="bubble-menu-arrow"></view>
|
|
263
|
+
</view>
|
|
264
|
+
</view>
|
|
265
|
+
|
|
246
266
|
<!-- 正在输入指示器 -->
|
|
247
267
|
<view wx:if="{{message.isStreaming}}" class="typing-indicator">
|
|
248
|
-
<!--
|
|
249
|
-
<
|
|
268
|
+
<!-- 语音播放中时显示播放动画,点击可停止 -->
|
|
269
|
+
<view wx:if="{{isPlaying}}" class="voice-playing-streaming" bindtap="onStopVoice">
|
|
250
270
|
<view class="voice-bar"></view>
|
|
251
271
|
<view class="voice-bar"></view>
|
|
252
272
|
<view class="voice-bar"></view>
|
|
253
|
-
</
|
|
273
|
+
</view>
|
|
254
274
|
<!-- 未播放时显示打字动画 -->
|
|
255
275
|
<block wx:else>
|
|
256
276
|
<view class="typing-dot"></view>
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
.ai-message {
|
|
17
17
|
flex-direction: row;
|
|
18
|
+
overflow: visible;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/* 头像 */
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
flex: 1;
|
|
47
48
|
align-items: flex-start;
|
|
48
49
|
max-width: calc(100%);
|
|
50
|
+
overflow: visible;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/* 消息气泡 */
|
|
@@ -67,7 +69,6 @@
|
|
|
67
69
|
.ai-bubble {
|
|
68
70
|
background-color: #fff;
|
|
69
71
|
color: #333;
|
|
70
|
-
border-bottom-left-radius: 8rpx;
|
|
71
72
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -132,8 +133,12 @@
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
.thinking-icon {
|
|
135
|
-
width: 32rpx;
|
|
136
|
-
height: 32rpx;
|
|
136
|
+
width: 32rpx !important;
|
|
137
|
+
height: 32rpx !important;
|
|
138
|
+
min-width: 32rpx !important;
|
|
139
|
+
min-height: 32rpx !important;
|
|
140
|
+
max-width: 32rpx !important;
|
|
141
|
+
max-height: 32rpx !important;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
.thinking-title {
|
|
@@ -267,12 +272,11 @@
|
|
|
267
272
|
overflow-x: auto;
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
/* 代码内容容器 -
|
|
275
|
+
/* 代码内容容器 - 使用 scroll-view 实现横向滚动 */
|
|
271
276
|
.code-content-wrapper {
|
|
272
277
|
padding: 20rpx;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
-webkit-overflow-scrolling: touch;
|
|
278
|
+
width: 100%;
|
|
279
|
+
box-sizing: border-box;
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
.code-text {
|
|
@@ -282,7 +286,6 @@
|
|
|
282
286
|
white-space: pre;
|
|
283
287
|
word-break: normal;
|
|
284
288
|
display: inline-block;
|
|
285
|
-
min-width: 100%;
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
/* 行内代码 */
|
|
@@ -461,8 +464,12 @@
|
|
|
461
464
|
}
|
|
462
465
|
|
|
463
466
|
.action-icon-img {
|
|
464
|
-
width: 40rpx;
|
|
465
|
-
height: 40rpx;
|
|
467
|
+
width: 40rpx !important;
|
|
468
|
+
height: 40rpx !important;
|
|
469
|
+
min-width: 40rpx !important;
|
|
470
|
+
min-height: 40rpx !important;
|
|
471
|
+
max-width: 40rpx !important;
|
|
472
|
+
max-height: 40rpx !important;
|
|
466
473
|
padding: 8rpx;
|
|
467
474
|
}
|
|
468
475
|
|
|
@@ -482,6 +489,17 @@
|
|
|
482
489
|
padding: 8rpx;
|
|
483
490
|
}
|
|
484
491
|
|
|
492
|
+
/* 流式消息中的语音播放动画(可点击停止) */
|
|
493
|
+
.voice-playing-streaming {
|
|
494
|
+
display: flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
justify-content: center;
|
|
497
|
+
gap: 4rpx;
|
|
498
|
+
padding: 8rpx 16rpx;
|
|
499
|
+
background-color: rgba(0, 122, 255, 0.1);
|
|
500
|
+
border-radius: 20rpx;
|
|
501
|
+
}
|
|
502
|
+
|
|
485
503
|
.voice-bar {
|
|
486
504
|
width: 6rpx;
|
|
487
505
|
height: 20rpx;
|
|
@@ -573,3 +591,76 @@
|
|
|
573
591
|
opacity: 0.5;
|
|
574
592
|
pointer-events: none;
|
|
575
593
|
}
|
|
594
|
+
|
|
595
|
+
/* 长按气泡菜单 */
|
|
596
|
+
.bubble-menu-overlay {
|
|
597
|
+
position: fixed;
|
|
598
|
+
top: 0;
|
|
599
|
+
left: 0;
|
|
600
|
+
right: 0;
|
|
601
|
+
bottom: 0;
|
|
602
|
+
z-index: 1000;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.bubble-menu {
|
|
606
|
+
position: absolute;
|
|
607
|
+
display: flex;
|
|
608
|
+
flex-direction: row;
|
|
609
|
+
background-color: #333;
|
|
610
|
+
border-radius: 16rpx;
|
|
611
|
+
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
|
|
612
|
+
padding: 16rpx 8rpx;
|
|
613
|
+
transform: translateX(-50%);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.bubble-menu-item {
|
|
617
|
+
display: flex;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
align-items: center;
|
|
620
|
+
justify-content: center;
|
|
621
|
+
padding: 16rpx 24rpx;
|
|
622
|
+
min-width: 120rpx;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.bubble-menu-item:active {
|
|
626
|
+
background-color: #444;
|
|
627
|
+
border-radius: 12rpx;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.bubble-menu-icon {
|
|
631
|
+
width: 48rpx !important;
|
|
632
|
+
height: 48rpx !important;
|
|
633
|
+
min-width: 48rpx !important;
|
|
634
|
+
min-height: 48rpx !important;
|
|
635
|
+
max-width: 48rpx !important;
|
|
636
|
+
max-height: 48rpx !important;
|
|
637
|
+
margin-bottom: 8rpx;
|
|
638
|
+
filter: brightness(0) invert(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.bubble-menu-icon-text {
|
|
642
|
+
font-size: 36rpx;
|
|
643
|
+
font-weight: bold;
|
|
644
|
+
color: #fff;
|
|
645
|
+
margin-bottom: 8rpx;
|
|
646
|
+
height: 48rpx;
|
|
647
|
+
line-height: 48rpx;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.bubble-menu-text {
|
|
651
|
+
font-size: 24rpx;
|
|
652
|
+
color: #fff;
|
|
653
|
+
white-space: nowrap;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.bubble-menu-arrow {
|
|
657
|
+
position: absolute;
|
|
658
|
+
bottom: -16rpx;
|
|
659
|
+
left: 50%;
|
|
660
|
+
transform: translateX(-50%);
|
|
661
|
+
width: 0;
|
|
662
|
+
height: 0;
|
|
663
|
+
border-left: 16rpx solid transparent;
|
|
664
|
+
border-right: 16rpx solid transparent;
|
|
665
|
+
border-top: 16rpx solid #333;
|
|
666
|
+
}
|
|
@@ -49,7 +49,17 @@ Component({
|
|
|
49
49
|
hasMore: true, // 是否有更多数据
|
|
50
50
|
currentPage: 1, // 当前页码
|
|
51
51
|
pageSize: 20, // 每页数量
|
|
52
|
-
searchKeyword: ''
|
|
52
|
+
searchKeyword: '', // 搜索关键词
|
|
53
|
+
// 胶囊按钮位置
|
|
54
|
+
menuButtonTop: 0,
|
|
55
|
+
menuButtonHeight: 0
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
lifetimes: {
|
|
59
|
+
attached() {
|
|
60
|
+
// 获取胶囊按钮位置,确保关闭按钮与其对齐
|
|
61
|
+
this.initMenuButtonInfo();
|
|
62
|
+
}
|
|
53
63
|
},
|
|
54
64
|
|
|
55
65
|
observers: {
|
|
@@ -61,6 +71,27 @@ Component({
|
|
|
61
71
|
},
|
|
62
72
|
|
|
63
73
|
methods: {
|
|
74
|
+
/**
|
|
75
|
+
* 获取胶囊按钮位置信息
|
|
76
|
+
*/
|
|
77
|
+
initMenuButtonInfo() {
|
|
78
|
+
try {
|
|
79
|
+
const menuButton = wx.getMenuButtonBoundingClientRect();
|
|
80
|
+
this.setData({
|
|
81
|
+
menuButtonTop: menuButton.top,
|
|
82
|
+
menuButtonHeight: menuButton.height
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// 降级处理
|
|
86
|
+
const systemInfo = wx.getSystemInfoSync();
|
|
87
|
+
const statusBarHeight = systemInfo.statusBarHeight || 20;
|
|
88
|
+
this.setData({
|
|
89
|
+
menuButtonTop: statusBarHeight + 4,
|
|
90
|
+
menuButtonHeight: 32
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
64
95
|
/**
|
|
65
96
|
* 加载会话列表
|
|
66
97
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<view class="sidebar-wrapper {{visible ? 'show' : ''}}">
|
|
2
2
|
<view class="sidebar-container" catchtap="preventBubble">
|
|
3
3
|
<!-- 顶部标题栏(只有关闭按钮) -->
|
|
4
|
-
<view class="sidebar-header">
|
|
5
|
-
<view class="close-btn" bindtap="onClose">
|
|
4
|
+
<view class="sidebar-header" style="padding-top: {{menuButtonTop}}px; height: {{menuButtonHeight}}px;">
|
|
5
|
+
<view class="close-btn" style="height: {{menuButtonHeight}}px;" bindtap="onClose">
|
|
6
6
|
<image class="close-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-close.svg" mode="aspectFit" />
|
|
7
7
|
</view>
|
|
8
8
|
</view>
|
|
@@ -30,21 +30,19 @@
|
|
|
30
30
|
display: flex;
|
|
31
31
|
align-items: center;
|
|
32
32
|
padding: 0 16rpx;
|
|
33
|
-
|
|
34
|
-
height: 88rpx;
|
|
33
|
+
box-sizing: content-box;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
.close-btn {
|
|
38
37
|
width: 64rpx;
|
|
39
|
-
height: 64rpx;
|
|
40
38
|
display: flex;
|
|
41
39
|
align-items: center;
|
|
42
40
|
justify-content: center;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
.close-icon {
|
|
46
|
-
width:
|
|
47
|
-
height:
|
|
44
|
+
width: 50rpx;
|
|
45
|
+
height: 50rpx;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
/* 标题行(对话列表 + 编辑按钮) */
|
|
@@ -82,6 +82,7 @@ Component({
|
|
|
82
82
|
keyboardHeight: 0, // 键盘高度
|
|
83
83
|
attachmentHeight: 0, // 附件预览区域高度
|
|
84
84
|
inputBarHeight: 0, // 输入框高度增量(多行时相对于单行的额外高度,rpx转px)
|
|
85
|
+
safeAreaBottom: 0, // 底部安全区域高度
|
|
85
86
|
|
|
86
87
|
// 会话相关
|
|
87
88
|
currentSession: {}, // 当前会话
|
|
@@ -98,6 +99,7 @@ Component({
|
|
|
98
99
|
// 加载状态
|
|
99
100
|
isStreaming: false, // 是否正在流式输出
|
|
100
101
|
lastScrollTime: 0, // 上次滚动时间(用于节流)
|
|
102
|
+
userScrolledUp: false, // 用户是否手动向上滚动(流式输出时暂停自动滚动)
|
|
101
103
|
hasMoreHistory: false, // 是否有更多历史消息
|
|
102
104
|
isLoadingHistory: false, // 是否正在加载历史消息
|
|
103
105
|
historyPage: 1, // 历史消息页码
|
|
@@ -110,6 +112,10 @@ Component({
|
|
|
110
112
|
editingContent: '', // 编辑中的内容
|
|
111
113
|
editingMessage: null, // 正在编辑的消息
|
|
112
114
|
|
|
115
|
+
// 节选复制弹框
|
|
116
|
+
showSelectCopyModal: false, // 是否显示节选复制弹框
|
|
117
|
+
selectCopySegments: [], // 节选内容段落
|
|
118
|
+
|
|
113
119
|
// 服务配置(从 serviceUrls 获取)
|
|
114
120
|
serviceUrls: {
|
|
115
121
|
aiChatUrl: '', // AI 聊天 API 地址
|
|
@@ -155,6 +161,20 @@ Component({
|
|
|
155
161
|
}
|
|
156
162
|
},
|
|
157
163
|
|
|
164
|
+
/**
|
|
165
|
+
* 页面生命周期(组件所在页面的生命周期)
|
|
166
|
+
*/
|
|
167
|
+
pageLifetimes: {
|
|
168
|
+
hide() {
|
|
169
|
+
// 页面隐藏时停止语音播放(如用户点击小程序关闭按钮)
|
|
170
|
+
audio.stop();
|
|
171
|
+
this.setData({
|
|
172
|
+
playingMessageId: null,
|
|
173
|
+
synthesizingMessageId: null
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
158
178
|
/**
|
|
159
179
|
* 组件的方法列表
|
|
160
180
|
*/
|
|
@@ -175,11 +195,15 @@ Component({
|
|
|
175
195
|
// 导航栏总高度 = 胶囊按钮底部 + 与顶部相同的边距
|
|
176
196
|
const navBarHeight = menuButton.bottom + (menuButton.top - statusBarHeight);
|
|
177
197
|
|
|
198
|
+
// 计算底部安全区域高度
|
|
199
|
+
const safeAreaBottom = systemInfo.screenHeight - systemInfo.safeArea.bottom;
|
|
200
|
+
|
|
178
201
|
this.setData({
|
|
179
202
|
statusBarHeight,
|
|
180
203
|
menuButtonTop,
|
|
181
204
|
menuButtonHeight,
|
|
182
|
-
navBarHeight
|
|
205
|
+
navBarHeight,
|
|
206
|
+
safeAreaBottom
|
|
183
207
|
});
|
|
184
208
|
},
|
|
185
209
|
|
|
@@ -194,7 +218,6 @@ Component({
|
|
|
194
218
|
console.log('[ChatBox] 未提供 data 参数');
|
|
195
219
|
return;
|
|
196
220
|
}
|
|
197
|
-
|
|
198
221
|
if (isToken) {
|
|
199
222
|
// data 就是 token,直接使用
|
|
200
223
|
this.setData({ userToken: data });
|
|
@@ -337,6 +360,13 @@ Component({
|
|
|
337
360
|
* 返回上一页
|
|
338
361
|
*/
|
|
339
362
|
onBack() {
|
|
363
|
+
// 停止语音播放
|
|
364
|
+
audio.stop();
|
|
365
|
+
this.setData({
|
|
366
|
+
playingMessageId: null,
|
|
367
|
+
synthesizingMessageId: null
|
|
368
|
+
});
|
|
369
|
+
|
|
340
370
|
// 触发 back 事件,让外部处理返回逻辑
|
|
341
371
|
this.triggerEvent('back');
|
|
342
372
|
|
|
@@ -682,6 +712,12 @@ Component({
|
|
|
682
712
|
|
|
683
713
|
if (!content && (!attachments || attachments.length === 0)) return;
|
|
684
714
|
|
|
715
|
+
// 停止正在播放的语音
|
|
716
|
+
if (this.data.playingMessageId) {
|
|
717
|
+
audio.stop();
|
|
718
|
+
this.setData({ playingMessageId: null, synthesizingMessageId: null });
|
|
719
|
+
}
|
|
720
|
+
|
|
685
721
|
// 检查登录状态
|
|
686
722
|
if (!this.getAiChatUrl() || !this.data.userToken) {
|
|
687
723
|
this.triggerEvent('login');
|
|
@@ -761,6 +797,7 @@ Component({
|
|
|
761
797
|
|
|
762
798
|
this.setData({
|
|
763
799
|
isStreaming: true,
|
|
800
|
+
userScrolledUp: false, // 重置用户滚动状态
|
|
764
801
|
streamingMessage: {
|
|
765
802
|
id: aiMessageId, // 使用固定 ID,便于跟踪播放状态
|
|
766
803
|
role: 'assistant',
|
|
@@ -986,6 +1023,12 @@ Component({
|
|
|
986
1023
|
return;
|
|
987
1024
|
}
|
|
988
1025
|
|
|
1026
|
+
// 停止正在播放的语音
|
|
1027
|
+
if (this.data.playingMessageId) {
|
|
1028
|
+
audio.stop();
|
|
1029
|
+
this.setData({ playingMessageId: null, synthesizingMessageId: null });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
989
1032
|
// 重新生成应该是针对用户消息的
|
|
990
1033
|
if (message.role !== 'user') {
|
|
991
1034
|
console.error('重新生成只能用于用户消息');
|
|
@@ -1053,6 +1096,12 @@ Component({
|
|
|
1053
1096
|
return;
|
|
1054
1097
|
}
|
|
1055
1098
|
|
|
1099
|
+
// 停止正在播放的语音
|
|
1100
|
+
if (this.data.playingMessageId) {
|
|
1101
|
+
audio.stop();
|
|
1102
|
+
this.setData({ playingMessageId: null, synthesizingMessageId: null });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1056
1105
|
this.setData({
|
|
1057
1106
|
editingContent: message.content,
|
|
1058
1107
|
editingMessage: message
|
|
@@ -1120,6 +1169,35 @@ Component({
|
|
|
1120
1169
|
});
|
|
1121
1170
|
},
|
|
1122
1171
|
|
|
1172
|
+
// ==================== 节选复制弹框 ====================
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* 打开节选复制弹框
|
|
1176
|
+
*/
|
|
1177
|
+
onSelectCopy(e) {
|
|
1178
|
+
const { segments } = e.detail;
|
|
1179
|
+
this.setData({
|
|
1180
|
+
showSelectCopyModal: true,
|
|
1181
|
+
selectCopySegments: segments
|
|
1182
|
+
});
|
|
1183
|
+
},
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* 关闭节选复制弹框
|
|
1187
|
+
*/
|
|
1188
|
+
hideSelectCopyModal() {
|
|
1189
|
+
this.setData({
|
|
1190
|
+
showSelectCopyModal: false
|
|
1191
|
+
});
|
|
1192
|
+
},
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* 阻止事件冒泡
|
|
1196
|
+
*/
|
|
1197
|
+
stopPropagation() {
|
|
1198
|
+
// 空函数,用于阻止事件冒泡
|
|
1199
|
+
},
|
|
1200
|
+
|
|
1123
1201
|
// ==================== 语音播放 ====================
|
|
1124
1202
|
|
|
1125
1203
|
/**
|
|
@@ -1144,6 +1222,30 @@ Component({
|
|
|
1144
1222
|
}
|
|
1145
1223
|
},
|
|
1146
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* 停止语音播放
|
|
1227
|
+
*/
|
|
1228
|
+
onStopVoice() {
|
|
1229
|
+
audio.stop();
|
|
1230
|
+
this.setData({
|
|
1231
|
+
playingMessageId: null,
|
|
1232
|
+
synthesizingMessageId: null
|
|
1233
|
+
});
|
|
1234
|
+
},
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* 按住说话开始时停止语音播放
|
|
1238
|
+
*/
|
|
1239
|
+
onVoiceRecordStart() {
|
|
1240
|
+
if (this.data.playingMessageId) {
|
|
1241
|
+
audio.stop();
|
|
1242
|
+
this.setData({
|
|
1243
|
+
playingMessageId: null,
|
|
1244
|
+
synthesizingMessageId: null
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
|
|
1147
1249
|
/**
|
|
1148
1250
|
* 播放消息语音
|
|
1149
1251
|
*/
|
|
@@ -1280,12 +1382,17 @@ Component({
|
|
|
1280
1382
|
|
|
1281
1383
|
/**
|
|
1282
1384
|
* 滚动到底部(带节流,流式输出时减少滚动频率)
|
|
1283
|
-
* @param {boolean} force -
|
|
1385
|
+
* @param {boolean} force - 是否强制滚动(不受节流限制和用户滚动状态限制)
|
|
1284
1386
|
*/
|
|
1285
1387
|
scrollToBottom(force = false) {
|
|
1286
1388
|
const now = Date.now();
|
|
1287
1389
|
const throttleInterval = 200; // 200ms 节流
|
|
1288
1390
|
|
|
1391
|
+
// 流式输出时,如果用户手动向上滚动了,不自动滚动(除非强制)
|
|
1392
|
+
if (!force && this.data.isStreaming && this.data.userScrolledUp) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1289
1396
|
// 非强制模式下,流式输出时进行节流
|
|
1290
1397
|
if (!force && this.data.isStreaming) {
|
|
1291
1398
|
if (now - this.data.lastScrollTime < throttleInterval) {
|
|
@@ -1318,6 +1425,39 @@ Component({
|
|
|
1318
1425
|
}
|
|
1319
1426
|
},
|
|
1320
1427
|
|
|
1428
|
+
/**
|
|
1429
|
+
* 滚动事件处理
|
|
1430
|
+
* 检测用户是否手动向上滚动
|
|
1431
|
+
*/
|
|
1432
|
+
onScroll(e) {
|
|
1433
|
+
// 只在流式输出时检测
|
|
1434
|
+
if (!this.data.isStreaming) {
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const { deltaY } = e.detail;
|
|
1439
|
+
|
|
1440
|
+
// deltaY > 0 表示向上滚动(手指向下滑)
|
|
1441
|
+
// 如果用户向上滚动,标记为手动滚动
|
|
1442
|
+
if (deltaY > 0) {
|
|
1443
|
+
if (!this.data.userScrolledUp) {
|
|
1444
|
+
this.setData({ userScrolledUp: true });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* 滚动到底部事件
|
|
1451
|
+
* 用户滚动到底部时,恢复自动滚动
|
|
1452
|
+
*/
|
|
1453
|
+
onScrollToLower() {
|
|
1454
|
+
if (this.data.isStreaming && this.data.userScrolledUp) {
|
|
1455
|
+
this.setData({ userScrolledUp: false });
|
|
1456
|
+
// 恢复后立即触发一次滚动,确保跟上最新数据
|
|
1457
|
+
this.scrollToBottom(true);
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
|
|
1321
1461
|
/**
|
|
1322
1462
|
* 格式化时间(完整格式:2025/09/18 17:41:22)
|
|
1323
1463
|
*/
|
|
@@ -87,12 +87,15 @@
|
|
|
87
87
|
<!-- 消息列表区域 -->
|
|
88
88
|
<scroll-view
|
|
89
89
|
class="message-list"
|
|
90
|
-
style="margin-top: {{navBarHeight}}px; padding-bottom: {{keyboardHeight > 0 ? (keyboardHeight + inputBarHeight + 40) : (
|
|
90
|
+
style="margin-top: {{navBarHeight}}px; padding-bottom: {{keyboardHeight > 0 ? (keyboardHeight + inputBarHeight + 40) : (50 + safeAreaBottom + attachmentHeight + inputBarHeight)}}px;"
|
|
91
91
|
scroll-y="true"
|
|
92
92
|
scroll-into-view="{{scrollToMessage}}"
|
|
93
93
|
enhanced="true"
|
|
94
94
|
show-scrollbar="false"
|
|
95
95
|
bindscrolltoupper="onScrollToUpper"
|
|
96
|
+
bindscroll="onScroll"
|
|
97
|
+
lower-threshold="50"
|
|
98
|
+
bindscrolltolower="onScrollToLower"
|
|
96
99
|
>
|
|
97
100
|
<!-- 加载更多 -->
|
|
98
101
|
<view wx:if="{{hasMoreHistory}}" class="load-more-history">
|
|
@@ -115,6 +118,7 @@
|
|
|
115
118
|
bind:edit="onEditMessage"
|
|
116
119
|
bind:quickreply="onQuickReply"
|
|
117
120
|
bind:imageload="onImageLoad"
|
|
121
|
+
bind:selectcopy="onSelectCopy"
|
|
118
122
|
/>
|
|
119
123
|
</block>
|
|
120
124
|
|
|
@@ -128,6 +132,7 @@
|
|
|
128
132
|
isPlaying="{{playingMessageId === streamingMessage.id}}"
|
|
129
133
|
isSynthesizing="{{synthesizingMessageId === streamingMessage.id}}"
|
|
130
134
|
isStreaming="{{true}}"
|
|
135
|
+
bind:stopvoice="onStopVoice"
|
|
131
136
|
/>
|
|
132
137
|
</view>
|
|
133
138
|
|
|
@@ -161,6 +166,7 @@
|
|
|
161
166
|
bind:keyboardheight="onKeyboardHeight"
|
|
162
167
|
bind:attachmentheight="onAttachmentHeight"
|
|
163
168
|
bind:inputheight="onInputHeight"
|
|
169
|
+
bind:voicerecordstart="onVoiceRecordStart"
|
|
164
170
|
/>
|
|
165
171
|
|
|
166
172
|
<!-- 免责声明(键盘弹出时隐藏) -->
|
|
@@ -169,4 +175,23 @@
|
|
|
169
175
|
</view>
|
|
170
176
|
</view>
|
|
171
177
|
</view>
|
|
178
|
+
|
|
179
|
+
<!-- 节选复制弹框(放在最外层,确保层级最高) -->
|
|
180
|
+
<view wx:if="{{showSelectCopyModal}}" class="select-copy-modal-overlay" catchtap="hideSelectCopyModal">
|
|
181
|
+
<view class="select-copy-modal" catchtap="stopPropagation">
|
|
182
|
+
<view class="select-copy-header">
|
|
183
|
+
<text class="select-copy-title">选择文字复制</text>
|
|
184
|
+
</view>
|
|
185
|
+
<scroll-view class="select-copy-content" scroll-y="true">
|
|
186
|
+
<block wx:for="{{selectCopySegments}}" wx:key="index">
|
|
187
|
+
<view class="select-copy-segment">
|
|
188
|
+
<text user-select="{{true}}" space="nbsp">{{item.text}}</text>
|
|
189
|
+
</view>
|
|
190
|
+
</block>
|
|
191
|
+
</scroll-view>
|
|
192
|
+
<view class="select-copy-footer">
|
|
193
|
+
<text class="select-copy-cancel" bindtap="hideSelectCopyModal">取消</text>
|
|
194
|
+
</view>
|
|
195
|
+
</view>
|
|
196
|
+
</view>
|
|
172
197
|
</view>
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
.message-wrapper {
|
|
219
|
-
padding-bottom:
|
|
219
|
+
padding-bottom: 40rpx;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/* 加载更多历史 */
|
|
@@ -289,3 +289,85 @@
|
|
|
289
289
|
font-size: 22rpx;
|
|
290
290
|
color: #999;
|
|
291
291
|
}
|
|
292
|
+
|
|
293
|
+
/* 节选复制弹框 */
|
|
294
|
+
.select-copy-modal-overlay {
|
|
295
|
+
position: fixed;
|
|
296
|
+
top: 0;
|
|
297
|
+
left: 0;
|
|
298
|
+
right: 0;
|
|
299
|
+
bottom: 0;
|
|
300
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
301
|
+
z-index: 9999;
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: flex-end;
|
|
304
|
+
justify-content: center;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.select-copy-modal {
|
|
308
|
+
width: 100%;
|
|
309
|
+
max-height: 70vh;
|
|
310
|
+
background-color: #fff;
|
|
311
|
+
border-radius: 24rpx 24rpx 0 0;
|
|
312
|
+
display: flex;
|
|
313
|
+
flex-direction: column;
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
animation: slideUp 0.3s ease-out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@keyframes slideUp {
|
|
319
|
+
from {
|
|
320
|
+
transform: translateY(100%);
|
|
321
|
+
}
|
|
322
|
+
to {
|
|
323
|
+
transform: translateY(0);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.select-copy-header {
|
|
328
|
+
padding: 32rpx;
|
|
329
|
+
border-bottom: 1rpx solid #eee;
|
|
330
|
+
text-align: center;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.select-copy-title {
|
|
334
|
+
font-size: 32rpx;
|
|
335
|
+
font-weight: 500;
|
|
336
|
+
color: #333;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.select-copy-content {
|
|
340
|
+
flex: 1;
|
|
341
|
+
padding: 24rpx 32rpx;
|
|
342
|
+
max-height: 50vh;
|
|
343
|
+
overflow-y: auto;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.select-copy-segment {
|
|
347
|
+
padding: 20rpx 24rpx;
|
|
348
|
+
margin-bottom: 16rpx;
|
|
349
|
+
background-color: #f8f9fa;
|
|
350
|
+
border-radius: 12rpx;
|
|
351
|
+
font-size: 28rpx;
|
|
352
|
+
line-height: 1.6;
|
|
353
|
+
color: #333;
|
|
354
|
+
-webkit-user-select: text;
|
|
355
|
+
user-select: text;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.select-copy-footer {
|
|
359
|
+
padding: 16rpx 32rpx;
|
|
360
|
+
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
|
361
|
+
display: flex;
|
|
362
|
+
justify-content: center;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.select-copy-cancel {
|
|
366
|
+
font-size: 28rpx;
|
|
367
|
+
color: #007aff;
|
|
368
|
+
padding: 16rpx 32rpx;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.select-copy-cancel:active {
|
|
372
|
+
opacity: 0.6;
|
|
373
|
+
}
|