@flexem/chat-box 1.0.1 → 1.0.3

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 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://oss-dev.platform.flexem.net`(OSS 服务)
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 // 元素 ID 计数器,用于生成唯一 ID
45
+ elementIdCounter: 0, // 元素 ID 计数器,用于生成唯一 ID
46
+ // 长按气泡菜单
47
+ showBubbleMenu: false,
48
+ bubbleMenuPosition: { top: 0, left: 0 }
46
49
  },
47
50
 
48
51
  observers: {
@@ -983,6 +986,131 @@ Component({
983
986
  if (option) {
984
987
  this.triggerEvent('quickreply', { content: option });
985
988
  }
989
+ },
990
+
991
+ // ==================== 长按气泡菜单 ====================
992
+
993
+ /**
994
+ * 长按消息显示气泡菜单
995
+ */
996
+ onLongPress(e) {
997
+ // 只对 AI 消息显示气泡菜单
998
+ if (this.properties.message.role !== 'assistant') {
999
+ return;
1000
+ }
1001
+
1002
+ // 获取触摸位置
1003
+ const touch = e.touches[0] || e.changedTouches[0];
1004
+ if (!touch) return;
1005
+
1006
+ // 计算气泡菜单位置(在触摸点上方)
1007
+ const menuHeight = 120; // 气泡菜单大约高度
1008
+ const top = touch.clientY - menuHeight - 20;
1009
+ const left = touch.clientX;
1010
+
1011
+ this.setData({
1012
+ showBubbleMenu: true,
1013
+ bubbleMenuPosition: {
1014
+ top: Math.max(top, 50), // 确保不超出顶部
1015
+ left: left
1016
+ }
1017
+ });
1018
+
1019
+ // 震动反馈
1020
+ wx.vibrateShort({ type: 'medium' });
1021
+ },
1022
+
1023
+ /**
1024
+ * 隐藏气泡菜单
1025
+ */
1026
+ hideBubbleMenu() {
1027
+ this.setData({ showBubbleMenu: false });
1028
+ },
1029
+
1030
+ /**
1031
+ * 阻止事件冒泡
1032
+ */
1033
+ stopPropagation() {
1034
+ // 空函数,用于阻止事件冒泡
1035
+ },
1036
+
1037
+ /**
1038
+ * 复制全文
1039
+ */
1040
+ onCopyAll() {
1041
+ const content = this.properties.message.content;
1042
+ wx.setClipboardData({
1043
+ data: content,
1044
+ success: () => {
1045
+ wx.showToast({
1046
+ title: '内容已复制',
1047
+ icon: 'success'
1048
+ });
1049
+ this.hideBubbleMenu();
1050
+ }
1051
+ });
1052
+ },
1053
+
1054
+ /**
1055
+ * 节选复制 - 触发事件让父组件显示弹框
1056
+ */
1057
+ onSelectCopy() {
1058
+ const content = this.properties.message.content;
1059
+ // 按段落分割内容
1060
+ const segments = this.splitContentToSegments(content);
1061
+
1062
+ this.setData({
1063
+ showBubbleMenu: false
1064
+ });
1065
+
1066
+ // 触发事件,让父组件显示弹框
1067
+ this.triggerEvent('selectcopy', { segments });
1068
+ },
1069
+
1070
+ /**
1071
+ * 将内容按段落分割
1072
+ */
1073
+ splitContentToSegments(content) {
1074
+ if (!content) return [];
1075
+
1076
+ // 按换行符分割,过滤空行,合并短段落
1077
+ const lines = content.split('\n');
1078
+ const segments = [];
1079
+ let currentSegment = '';
1080
+
1081
+ for (const line of lines) {
1082
+ const trimmedLine = line.trim();
1083
+ if (!trimmedLine) {
1084
+ // 空行,保存当前段落
1085
+ if (currentSegment) {
1086
+ segments.push({ text: currentSegment.trim() });
1087
+ currentSegment = '';
1088
+ }
1089
+ } else {
1090
+ // 非空行
1091
+ if (currentSegment) {
1092
+ currentSegment += '\n' + trimmedLine;
1093
+ } else {
1094
+ currentSegment = trimmedLine;
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ // 保存最后一个段落
1100
+ if (currentSegment) {
1101
+ segments.push({ text: currentSegment.trim() });
1102
+ }
1103
+
1104
+ return segments;
1105
+ },
1106
+
1107
+ /**
1108
+ * 气泡菜单 - 语音播放
1109
+ */
1110
+ onBubblePlayVoice() {
1111
+ this.hideBubbleMenu();
1112
+ const message = this.properties.message;
1113
+ this.triggerEvent('playvoice', { id: message.id, content: message.content });
986
1114
  }
987
1115
  }
988
1116
  });
@@ -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">
@@ -243,6 +243,26 @@
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
+ <image class="bubble-menu-icon" src="../../assets/icons/icon-edit-msg.svg" mode="aspectFit" />
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
268
  <!-- 语音播放中时显示播放动画 -->
@@ -573,3 +573,63 @@
573
573
  opacity: 0.5;
574
574
  pointer-events: none;
575
575
  }
576
+
577
+ /* 长按气泡菜单 */
578
+ .bubble-menu-overlay {
579
+ position: fixed;
580
+ top: 0;
581
+ left: 0;
582
+ right: 0;
583
+ bottom: 0;
584
+ z-index: 1000;
585
+ }
586
+
587
+ .bubble-menu {
588
+ position: absolute;
589
+ display: flex;
590
+ flex-direction: row;
591
+ background-color: #333;
592
+ border-radius: 16rpx;
593
+ box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
594
+ padding: 16rpx 8rpx;
595
+ transform: translateX(-50%);
596
+ }
597
+
598
+ .bubble-menu-item {
599
+ display: flex;
600
+ flex-direction: column;
601
+ align-items: center;
602
+ justify-content: center;
603
+ padding: 16rpx 24rpx;
604
+ min-width: 120rpx;
605
+ }
606
+
607
+ .bubble-menu-item:active {
608
+ background-color: #444;
609
+ border-radius: 12rpx;
610
+ }
611
+
612
+ .bubble-menu-icon {
613
+ width: 48rpx;
614
+ height: 48rpx;
615
+ margin-bottom: 8rpx;
616
+ filter: brightness(0) invert(1);
617
+ }
618
+
619
+ .bubble-menu-text {
620
+ font-size: 24rpx;
621
+ color: #fff;
622
+ white-space: nowrap;
623
+ }
624
+
625
+ .bubble-menu-arrow {
626
+ position: absolute;
627
+ bottom: -16rpx;
628
+ left: 50%;
629
+ transform: translateX(-50%);
630
+ width: 0;
631
+ height: 0;
632
+ border-left: 16rpx solid transparent;
633
+ border-right: 16rpx solid transparent;
634
+ border-top: 16rpx solid #333;
635
+ }
@@ -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
- padding-top: env(safe-area-inset-top);
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: 52rpx;
47
- height: 52rpx;
44
+ width: 50rpx;
45
+ height: 50rpx;
48
46
  }
49
47
 
50
48
  /* 标题行(对话列表 + 编辑按钮) */
@@ -110,6 +110,10 @@ Component({
110
110
  editingContent: '', // 编辑中的内容
111
111
  editingMessage: null, // 正在编辑的消息
112
112
 
113
+ // 节选复制弹框
114
+ showSelectCopyModal: false, // 是否显示节选复制弹框
115
+ selectCopySegments: [], // 节选内容段落
116
+
113
117
  // 服务配置(从 serviceUrls 获取)
114
118
  serviceUrls: {
115
119
  aiChatUrl: '', // AI 聊天 API 地址
@@ -194,7 +198,6 @@ Component({
194
198
  console.log('[ChatBox] 未提供 data 参数');
195
199
  return;
196
200
  }
197
-
198
201
  if (isToken) {
199
202
  // data 就是 token,直接使用
200
203
  this.setData({ userToken: data });
@@ -1120,6 +1123,35 @@ Component({
1120
1123
  });
1121
1124
  },
1122
1125
 
1126
+ // ==================== 节选复制弹框 ====================
1127
+
1128
+ /**
1129
+ * 打开节选复制弹框
1130
+ */
1131
+ onSelectCopy(e) {
1132
+ const { segments } = e.detail;
1133
+ this.setData({
1134
+ showSelectCopyModal: true,
1135
+ selectCopySegments: segments
1136
+ });
1137
+ },
1138
+
1139
+ /**
1140
+ * 关闭节选复制弹框
1141
+ */
1142
+ hideSelectCopyModal() {
1143
+ this.setData({
1144
+ showSelectCopyModal: false
1145
+ });
1146
+ },
1147
+
1148
+ /**
1149
+ * 阻止事件冒泡
1150
+ */
1151
+ stopPropagation() {
1152
+ // 空函数,用于阻止事件冒泡
1153
+ },
1154
+
1123
1155
  // ==================== 语音播放 ====================
1124
1156
 
1125
1157
  /**
@@ -115,6 +115,7 @@
115
115
  bind:edit="onEditMessage"
116
116
  bind:quickreply="onQuickReply"
117
117
  bind:imageload="onImageLoad"
118
+ bind:selectcopy="onSelectCopy"
118
119
  />
119
120
  </block>
120
121
 
@@ -169,4 +170,23 @@
169
170
  </view>
170
171
  </view>
171
172
  </view>
173
+
174
+ <!-- 节选复制弹框(放在最外层,确保层级最高) -->
175
+ <view wx:if="{{showSelectCopyModal}}" class="select-copy-modal-overlay" catchtap="hideSelectCopyModal">
176
+ <view class="select-copy-modal" catchtap="stopPropagation">
177
+ <view class="select-copy-header">
178
+ <text class="select-copy-title">选择文字复制</text>
179
+ </view>
180
+ <scroll-view class="select-copy-content" scroll-y="true">
181
+ <block wx:for="{{selectCopySegments}}" wx:key="index">
182
+ <view class="select-copy-segment">
183
+ <text user-select="{{true}}" space="nbsp">{{item.text}}</text>
184
+ </view>
185
+ </block>
186
+ </scroll-view>
187
+ <view class="select-copy-footer">
188
+ <text class="select-copy-cancel" bindtap="hideSelectCopyModal">取消</text>
189
+ </view>
190
+ </view>
191
+ </view>
172
192
  </view>
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@flexem/chat-box",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "main": "index.js"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexem/chat-box",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "chat box",
5
5
  "main": "miniprogram_dist/index.js",
6
6
  "miniprogram": "miniprogram_dist",