@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.
- package/README.md +638 -0
- package/miniprogram_dist/TEST_CASES.md +256 -0
- package/miniprogram_dist/assets/icons/icon-arrow-down.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-arrow-up.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-avatar-default.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-back.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-camera.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-close.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-copy.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-delete.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-edit-msg.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-edit.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-file.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-image.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-keyboard.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-menu.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-play-voice.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-plus.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-regenerate.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-thinking.svg +1 -0
- package/miniprogram_dist/assets/icons/icon-voice.svg +1 -0
- package/miniprogram_dist/components/attachment/index.js +169 -0
- package/miniprogram_dist/components/attachment/index.json +4 -0
- package/miniprogram_dist/components/attachment/index.wxml +40 -0
- package/miniprogram_dist/components/attachment/index.wxss +119 -0
- package/miniprogram_dist/components/input-bar/index.js +934 -0
- package/miniprogram_dist/components/input-bar/index.json +6 -0
- package/miniprogram_dist/components/input-bar/index.wxml +132 -0
- package/miniprogram_dist/components/input-bar/index.wxss +324 -0
- package/miniprogram_dist/components/message/index.js +988 -0
- package/miniprogram_dist/components/message/index.json +4 -0
- package/miniprogram_dist/components/message/index.wxml +285 -0
- package/miniprogram_dist/components/message/index.wxss +575 -0
- package/miniprogram_dist/components/sidebar/index.js +506 -0
- package/miniprogram_dist/components/sidebar/index.json +4 -0
- package/miniprogram_dist/components/sidebar/index.wxml +137 -0
- package/miniprogram_dist/components/sidebar/index.wxss +264 -0
- package/miniprogram_dist/index.js +1316 -0
- package/miniprogram_dist/index.json +8 -0
- package/miniprogram_dist/index.wxml +172 -0
- package/miniprogram_dist/index.wxss +291 -0
- package/miniprogram_dist/package.json +5 -0
- package/miniprogram_dist/utils/api.js +474 -0
- package/miniprogram_dist/utils/audio.js +860 -0
- package/miniprogram_dist/utils/storage.js +168 -0
- package/package.json +27 -0
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息组件
|
|
3
|
+
* 支持 Markdown 渲染、图片预览、语音播放等
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
Component({
|
|
7
|
+
properties: {
|
|
8
|
+
// 消息数据
|
|
9
|
+
message: {
|
|
10
|
+
type: Object,
|
|
11
|
+
value: {}
|
|
12
|
+
},
|
|
13
|
+
// 用户头像
|
|
14
|
+
userAvatar: {
|
|
15
|
+
type: String,
|
|
16
|
+
value: ''
|
|
17
|
+
},
|
|
18
|
+
// 是否显示用户操作按钮
|
|
19
|
+
showUserActions: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
value: true
|
|
22
|
+
},
|
|
23
|
+
// 是否正在播放语音
|
|
24
|
+
isPlaying: {
|
|
25
|
+
type: Boolean,
|
|
26
|
+
value: false
|
|
27
|
+
},
|
|
28
|
+
// 是否正在合成语音
|
|
29
|
+
isSynthesizing: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
value: false
|
|
32
|
+
},
|
|
33
|
+
// 是否正在流式生成(用于禁用快捷回复按钮)
|
|
34
|
+
isStreaming: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
value: false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
data: {
|
|
41
|
+
parsedContent: [], // 解析后的 Markdown 内容
|
|
42
|
+
showThinking: false, // 是否显示思考过程
|
|
43
|
+
lastParseTime: 0, // 上次解析时间
|
|
44
|
+
pendingParse: false, // 是否有待处理的解析
|
|
45
|
+
elementIdCounter: 0 // 元素 ID 计数器,用于生成唯一 ID
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
observers: {
|
|
49
|
+
'message.content': function(content) {
|
|
50
|
+
if (content && this.properties.message.role === 'assistant') {
|
|
51
|
+
this.throttledParseMarkdown(content);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
'message.isStreaming': function(isStreaming) {
|
|
55
|
+
// 流式输出结束时,立即执行最后一次解析
|
|
56
|
+
if (!isStreaming && this.data.pendingParse) {
|
|
57
|
+
this.parseMarkdown(this.properties.message.content);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
lifetimes: {
|
|
63
|
+
attached() {
|
|
64
|
+
const message = this.properties.message;
|
|
65
|
+
if (message.content && message.role === 'assistant') {
|
|
66
|
+
this.parseMarkdown(message.content);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
detached() {
|
|
70
|
+
// 清理定时器
|
|
71
|
+
if (this.parseTimer) {
|
|
72
|
+
clearTimeout(this.parseTimer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
methods: {
|
|
78
|
+
/**
|
|
79
|
+
* 节流解析 Markdown(流式输出时减少解析频率)
|
|
80
|
+
*/
|
|
81
|
+
throttledParseMarkdown(content) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const isStreaming = this.properties.message.isStreaming;
|
|
84
|
+
|
|
85
|
+
// 非流式输出时直接解析
|
|
86
|
+
if (!isStreaming) {
|
|
87
|
+
this.parseMarkdown(content, false);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 流式输出时,使用节流(每150ms最多解析一次)
|
|
92
|
+
const throttleInterval = 150;
|
|
93
|
+
|
|
94
|
+
if (now - this.data.lastParseTime >= throttleInterval) {
|
|
95
|
+
this.parseMarkdown(content, true);
|
|
96
|
+
this.setData({ lastParseTime: now, pendingParse: false });
|
|
97
|
+
} else {
|
|
98
|
+
// 标记有待处理的解析
|
|
99
|
+
this.setData({ pendingParse: true });
|
|
100
|
+
|
|
101
|
+
// 设置延迟解析
|
|
102
|
+
if (this.parseTimer) {
|
|
103
|
+
clearTimeout(this.parseTimer);
|
|
104
|
+
}
|
|
105
|
+
this.parseTimer = setTimeout(() => {
|
|
106
|
+
this.parseMarkdown(content, true);
|
|
107
|
+
this.setData({ lastParseTime: Date.now(), pendingParse: false });
|
|
108
|
+
}, throttleInterval);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 简易 Markdown 解析器
|
|
114
|
+
* @param {string} content - Markdown 内容
|
|
115
|
+
* @param {boolean} isStreaming - 是否处于流式输出模式
|
|
116
|
+
*/
|
|
117
|
+
parseMarkdown(content, isStreaming = false) {
|
|
118
|
+
if (!content) {
|
|
119
|
+
this.setData({ parsedContent: [] });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 预处理:修复跨行的链接 [text]\n(url) -> [text](url)
|
|
124
|
+
let processedContent = this.preprocessLinks(content);
|
|
125
|
+
|
|
126
|
+
const lines = processedContent.split('\n');
|
|
127
|
+
const result = [];
|
|
128
|
+
let i = 0;
|
|
129
|
+
let inCodeBlock = false;
|
|
130
|
+
let codeBlockLang = '';
|
|
131
|
+
let codeBlockContent = [];
|
|
132
|
+
let elementId = 0; // 元素 ID 计数器
|
|
133
|
+
|
|
134
|
+
while (i < lines.length) {
|
|
135
|
+
const line = lines[i];
|
|
136
|
+
|
|
137
|
+
// 代码块开始/结束
|
|
138
|
+
if (line.startsWith('```')) {
|
|
139
|
+
if (inCodeBlock) {
|
|
140
|
+
// 代码块结束
|
|
141
|
+
const codeText = codeBlockContent.join('\n');
|
|
142
|
+
|
|
143
|
+
// 特殊处理 quick-replies 代码块
|
|
144
|
+
if (codeBlockLang === 'quick-replies') {
|
|
145
|
+
const trimmedCode = codeText.trim();
|
|
146
|
+
if (trimmedCode && trimmedCode.startsWith('[') && trimmedCode.endsWith(']')) {
|
|
147
|
+
try {
|
|
148
|
+
const options = JSON.parse(trimmedCode);
|
|
149
|
+
if (Array.isArray(options) && options.length > 0) {
|
|
150
|
+
result.push({
|
|
151
|
+
id: 'quick-replies_' + elementId++,
|
|
152
|
+
type: 'quick-replies',
|
|
153
|
+
options: options
|
|
154
|
+
});
|
|
155
|
+
inCodeBlock = false;
|
|
156
|
+
codeBlockContent = [];
|
|
157
|
+
codeBlockLang = '';
|
|
158
|
+
i++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// JSON 解析失败,流式传输中数据可能不完整,静默忽略
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 如果数据不完整或无效,不渲染(等待完整数据)
|
|
166
|
+
inCodeBlock = false;
|
|
167
|
+
codeBlockContent = [];
|
|
168
|
+
codeBlockLang = '';
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 普通代码块
|
|
174
|
+
result.push({
|
|
175
|
+
id: 'code_' + elementId++,
|
|
176
|
+
type: 'code',
|
|
177
|
+
lang: codeBlockLang,
|
|
178
|
+
text: codeText
|
|
179
|
+
});
|
|
180
|
+
inCodeBlock = false;
|
|
181
|
+
codeBlockContent = [];
|
|
182
|
+
codeBlockLang = '';
|
|
183
|
+
} else {
|
|
184
|
+
// 代码块开始
|
|
185
|
+
inCodeBlock = true;
|
|
186
|
+
codeBlockLang = line.slice(3).trim();
|
|
187
|
+
}
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 在代码块内
|
|
193
|
+
if (inCodeBlock) {
|
|
194
|
+
codeBlockContent.push(line);
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 空行
|
|
200
|
+
if (line.trim() === '') {
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 分隔线
|
|
206
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
|
|
207
|
+
result.push({ id: 'hr_' + elementId++, type: 'hr' });
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 表格检测(以 | 开头或包含 | 的行)
|
|
213
|
+
if (line.includes('|') && this.isTableStart(lines, i)) {
|
|
214
|
+
const tableResult = this.parseTable(lines, i);
|
|
215
|
+
if (tableResult.table) {
|
|
216
|
+
tableResult.table.id = 'table_' + elementId++;
|
|
217
|
+
result.push(tableResult.table);
|
|
218
|
+
i = tableResult.nextIndex;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 标题
|
|
224
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
225
|
+
if (headingMatch) {
|
|
226
|
+
result.push({
|
|
227
|
+
id: 'heading_' + elementId++,
|
|
228
|
+
type: 'heading',
|
|
229
|
+
level: headingMatch[1].length,
|
|
230
|
+
text: headingMatch[2]
|
|
231
|
+
});
|
|
232
|
+
i++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 引用
|
|
237
|
+
if (line.startsWith('>')) {
|
|
238
|
+
let quoteText = line.slice(1).trim();
|
|
239
|
+
i++;
|
|
240
|
+
// 收集连续的引用行
|
|
241
|
+
while (i < lines.length && lines[i].startsWith('>')) {
|
|
242
|
+
quoteText += '\n' + lines[i].slice(1).trim();
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
result.push({
|
|
246
|
+
id: 'quote_' + elementId++,
|
|
247
|
+
type: 'blockquote',
|
|
248
|
+
text: this.processInlineElements(quoteText)
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 无序列表(支持嵌套)
|
|
254
|
+
const unorderedMatch = line.match(/^(\s*)([-*+])\s+(.+)$/);
|
|
255
|
+
if (unorderedMatch) {
|
|
256
|
+
const listResult = this.parseNestedList(lines, i, false);
|
|
257
|
+
listResult.list.id = 'list_' + elementId++;
|
|
258
|
+
result.push(listResult.list);
|
|
259
|
+
i = listResult.nextIndex;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 有序列表(支持嵌套)
|
|
264
|
+
const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
265
|
+
if (orderedMatch) {
|
|
266
|
+
const listResult = this.parseNestedList(lines, i, true);
|
|
267
|
+
listResult.list.id = 'list_' + elementId++;
|
|
268
|
+
result.push(listResult.list);
|
|
269
|
+
i = listResult.nextIndex;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 图片(独立行)
|
|
274
|
+
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
275
|
+
if (imageMatch) {
|
|
276
|
+
result.push({
|
|
277
|
+
id: 'image_' + elementId++,
|
|
278
|
+
type: 'image',
|
|
279
|
+
alt: imageMatch[1],
|
|
280
|
+
src: imageMatch[2]
|
|
281
|
+
});
|
|
282
|
+
i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 普通段落(处理行内元素)
|
|
287
|
+
result.push({
|
|
288
|
+
id: 'para_' + elementId++,
|
|
289
|
+
type: 'paragraph',
|
|
290
|
+
text: this.processInlineElements(line)
|
|
291
|
+
});
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 处理未闭合的代码块(流式输出时代码块可能未闭合)
|
|
296
|
+
if (inCodeBlock && codeBlockContent.length > 0) {
|
|
297
|
+
result.push({
|
|
298
|
+
id: 'code_streaming', // 使用固定 ID,这样流式更新时不会重建元素
|
|
299
|
+
type: 'code',
|
|
300
|
+
lang: codeBlockLang,
|
|
301
|
+
text: codeBlockContent.join('\n'),
|
|
302
|
+
isStreamingCode: true // 标记为流式输出中的代码块
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 流式输出时使用增量更新策略
|
|
307
|
+
if (isStreaming && this.data.parsedContent.length > 0) {
|
|
308
|
+
const oldContent = this.data.parsedContent;
|
|
309
|
+
const newContent = result;
|
|
310
|
+
|
|
311
|
+
// 检查是否只是最后一个元素变化了(代码块内容增加)
|
|
312
|
+
const lastOldItem = oldContent[oldContent.length - 1];
|
|
313
|
+
const lastNewItem = newContent[newContent.length - 1];
|
|
314
|
+
|
|
315
|
+
// 如果前面的元素数量相同,且最后一个是流式代码块,只更新文本内容
|
|
316
|
+
if (oldContent.length === newContent.length &&
|
|
317
|
+
lastOldItem && lastNewItem &&
|
|
318
|
+
lastOldItem.id === lastNewItem.id &&
|
|
319
|
+
lastNewItem.isStreamingCode) {
|
|
320
|
+
// 只更新代码块的 text 内容
|
|
321
|
+
this.setData({
|
|
322
|
+
[`parsedContent[${newContent.length - 1}].text`]: lastNewItem.text
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 如果前面的元素数量相同,且最后一个是普通段落,只更新最后一个元素的 text
|
|
328
|
+
if (oldContent.length === newContent.length &&
|
|
329
|
+
lastOldItem && lastNewItem &&
|
|
330
|
+
lastOldItem.id === lastNewItem.id &&
|
|
331
|
+
lastNewItem.type === 'paragraph') {
|
|
332
|
+
// 只更新最后一个段落的内容
|
|
333
|
+
this.setData({
|
|
334
|
+
[`parsedContent[${newContent.length - 1}].text`]: lastNewItem.text
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 如果新增了一个元素(代码块刚开始),追加而不是替换
|
|
340
|
+
if (newContent.length === oldContent.length + 1) {
|
|
341
|
+
const allPreviousSame = oldContent.every((item, idx) => {
|
|
342
|
+
return item.id === newContent[idx]?.id;
|
|
343
|
+
});
|
|
344
|
+
if (allPreviousSame) {
|
|
345
|
+
// 追加新元素
|
|
346
|
+
this.setData({
|
|
347
|
+
[`parsedContent[${newContent.length - 1}]`]: lastNewItem
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 默认:完整替换
|
|
355
|
+
this.setData({ parsedContent: result });
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 检测是否是表格开始
|
|
360
|
+
*/
|
|
361
|
+
isTableStart(lines, index) {
|
|
362
|
+
if (index + 1 >= lines.length) return false;
|
|
363
|
+
const currentLine = lines[index];
|
|
364
|
+
const nextLine = lines[index + 1];
|
|
365
|
+
// 表格需要有分隔行(包含 | 和 - 或 :)
|
|
366
|
+
return currentLine.includes('|') && /^[\s|:-]+$/.test(nextLine) && nextLine.includes('-');
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 解析表格
|
|
371
|
+
*/
|
|
372
|
+
parseTable(lines, startIndex) {
|
|
373
|
+
const headerLine = lines[startIndex];
|
|
374
|
+
const separatorLine = lines[startIndex + 1];
|
|
375
|
+
|
|
376
|
+
// 解析表头
|
|
377
|
+
const headers = this.parseTableRow(headerLine);
|
|
378
|
+
if (headers.length === 0) {
|
|
379
|
+
return { table: null, nextIndex: startIndex + 1 };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 解析对齐方式
|
|
383
|
+
const alignments = this.parseTableAlignments(separatorLine);
|
|
384
|
+
|
|
385
|
+
// 解析数据行
|
|
386
|
+
const rows = [];
|
|
387
|
+
let i = startIndex + 2;
|
|
388
|
+
while (i < lines.length) {
|
|
389
|
+
const line = lines[i];
|
|
390
|
+
if (!line.includes('|') || line.trim() === '') {
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
const row = this.parseTableRow(line);
|
|
394
|
+
if (row.length > 0) {
|
|
395
|
+
rows.push(row);
|
|
396
|
+
}
|
|
397
|
+
i++;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
table: {
|
|
402
|
+
type: 'table',
|
|
403
|
+
headers: headers,
|
|
404
|
+
alignments: alignments,
|
|
405
|
+
rows: rows
|
|
406
|
+
},
|
|
407
|
+
nextIndex: i
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 解析表格行
|
|
413
|
+
*/
|
|
414
|
+
parseTableRow(line) {
|
|
415
|
+
// 去掉首尾的 |
|
|
416
|
+
let trimmed = line.trim();
|
|
417
|
+
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
|
|
418
|
+
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
|
|
419
|
+
|
|
420
|
+
// 按 | 分割并处理每个单元格
|
|
421
|
+
return trimmed.split('|').map(cell => this.processInlineElements(cell.trim()));
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 解析表格对齐方式
|
|
426
|
+
*/
|
|
427
|
+
parseTableAlignments(line) {
|
|
428
|
+
let trimmed = line.trim();
|
|
429
|
+
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
|
|
430
|
+
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
|
|
431
|
+
|
|
432
|
+
return trimmed.split('|').map(cell => {
|
|
433
|
+
const c = cell.trim();
|
|
434
|
+
if (c.startsWith(':') && c.endsWith(':')) return 'center';
|
|
435
|
+
if (c.endsWith(':')) return 'right';
|
|
436
|
+
return 'left';
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 解析嵌套列表
|
|
442
|
+
* @param {string[]} lines - 所有行
|
|
443
|
+
* @param {number} startIndex - 起始行索引
|
|
444
|
+
* @param {boolean} isOrdered - 是否是有序列表
|
|
445
|
+
* @returns {{ list: Object, nextIndex: number }}
|
|
446
|
+
*/
|
|
447
|
+
parseNestedList(lines, startIndex, isOrdered) {
|
|
448
|
+
const items = [];
|
|
449
|
+
let i = startIndex;
|
|
450
|
+
|
|
451
|
+
// 获取第一行的缩进作为基准
|
|
452
|
+
const firstLine = lines[i];
|
|
453
|
+
const baseIndentMatch = firstLine.match(/^(\s*)/);
|
|
454
|
+
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
455
|
+
|
|
456
|
+
while (i < lines.length) {
|
|
457
|
+
const line = lines[i];
|
|
458
|
+
|
|
459
|
+
// 跳过空行
|
|
460
|
+
if (line.trim() === '') {
|
|
461
|
+
i++;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 计算当前行缩进
|
|
466
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
467
|
+
const currentIndent = indentMatch ? indentMatch[1].length : 0;
|
|
468
|
+
|
|
469
|
+
// 匹配列表项(无序或有序)
|
|
470
|
+
const unorderedMatch = line.match(/^(\s*)([-*+])\s+(.+)$/);
|
|
471
|
+
const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
472
|
+
const listMatch = unorderedMatch || orderedMatch;
|
|
473
|
+
|
|
474
|
+
if (!listMatch) {
|
|
475
|
+
// 不是列表项,结束列表解析
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const itemIndent = listMatch[1].length;
|
|
480
|
+
|
|
481
|
+
// 如果缩进小于基准缩进,说明这是外层列表或结束
|
|
482
|
+
if (itemIndent < baseIndent) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 如果缩进等于基准缩进,是同级列表项
|
|
487
|
+
if (itemIndent === baseIndent) {
|
|
488
|
+
const content = unorderedMatch ? unorderedMatch[3] : orderedMatch[3];
|
|
489
|
+
const itemIsOrdered = !!orderedMatch;
|
|
490
|
+
|
|
491
|
+
items.push({
|
|
492
|
+
content: this.processInlineElements(content),
|
|
493
|
+
indent: 0,
|
|
494
|
+
isOrdered: itemIsOrdered,
|
|
495
|
+
children: []
|
|
496
|
+
});
|
|
497
|
+
i++;
|
|
498
|
+
}
|
|
499
|
+
// 如果缩进大于基准缩进,是子列表项
|
|
500
|
+
else if (itemIndent > baseIndent) {
|
|
501
|
+
// 递归解析子列表
|
|
502
|
+
const subListResult = this.parseNestedList(lines, i, !!orderedMatch);
|
|
503
|
+
|
|
504
|
+
// 将子列表添加到最后一个列表项的 children 中
|
|
505
|
+
if (items.length > 0) {
|
|
506
|
+
items[items.length - 1].children.push(subListResult.list);
|
|
507
|
+
}
|
|
508
|
+
i = subListResult.nextIndex;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
list: {
|
|
514
|
+
type: 'list',
|
|
515
|
+
ordered: isOrdered,
|
|
516
|
+
items: items
|
|
517
|
+
},
|
|
518
|
+
nextIndex: i
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 处理行内元素,返回结构化数组
|
|
524
|
+
* 支持:加粗、斜体、链接(Markdown和HTML格式)、行内代码、行内图片
|
|
525
|
+
*/
|
|
526
|
+
processInlineElements(text) {
|
|
527
|
+
const parts = [];
|
|
528
|
+
let remaining = text;
|
|
529
|
+
let currentIndex = 0;
|
|
530
|
+
|
|
531
|
+
while (remaining.length > 0) {
|
|
532
|
+
// 查找最近的特殊标记
|
|
533
|
+
const linkStart = remaining.indexOf('[');
|
|
534
|
+
const imageStart = remaining.indexOf('![');
|
|
535
|
+
const boldStart = remaining.indexOf('**');
|
|
536
|
+
const boldStart2 = remaining.indexOf('__');
|
|
537
|
+
const codeStart = remaining.indexOf('`');
|
|
538
|
+
const htmlLinkStart = remaining.indexOf('<a ');
|
|
539
|
+
|
|
540
|
+
// 查找斜体标记(单个 * 或 _,但不是 ** 或 __)
|
|
541
|
+
let italicStart = -1;
|
|
542
|
+
let italicStart2 = -1;
|
|
543
|
+
|
|
544
|
+
// 查找单个 * 斜体(不是 ** 的一部分)
|
|
545
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
546
|
+
if (remaining[i] === '*') {
|
|
547
|
+
// 检查是否是 ** 的一部分
|
|
548
|
+
if (remaining[i + 1] !== '*' && (i === 0 || remaining[i - 1] !== '*')) {
|
|
549
|
+
italicStart = i;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 查找单个 _ 斜体(不是 __ 的一部分)
|
|
556
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
557
|
+
if (remaining[i] === '_') {
|
|
558
|
+
// 检查是否是 __ 的一部分
|
|
559
|
+
if (remaining[i + 1] !== '_' && (i === 0 || remaining[i - 1] !== '_')) {
|
|
560
|
+
italicStart2 = i;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// 找到最近的标记位置
|
|
567
|
+
const positions = [
|
|
568
|
+
{ type: 'image', pos: imageStart },
|
|
569
|
+
{ type: 'link', pos: linkStart },
|
|
570
|
+
{ type: 'bold', pos: boldStart },
|
|
571
|
+
{ type: 'bold2', pos: boldStart2 },
|
|
572
|
+
{ type: 'italic', pos: italicStart },
|
|
573
|
+
{ type: 'italic2', pos: italicStart2 },
|
|
574
|
+
{ type: 'code', pos: codeStart },
|
|
575
|
+
{ type: 'htmlLink', pos: htmlLinkStart }
|
|
576
|
+
].filter(p => p.pos !== -1).sort((a, b) => a.pos - b.pos);
|
|
577
|
+
|
|
578
|
+
if (positions.length === 0) {
|
|
579
|
+
// 没有特殊标记,剩余全是普通文本
|
|
580
|
+
if (remaining.length > 0) {
|
|
581
|
+
parts.push({ type: 'text', text: remaining, bold: false });
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const nearest = positions[0];
|
|
587
|
+
|
|
588
|
+
// 添加标记前的普通文本
|
|
589
|
+
if (nearest.pos > 0) {
|
|
590
|
+
parts.push({ type: 'text', text: remaining.slice(0, nearest.pos), bold: false });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (nearest.type === 'image') {
|
|
594
|
+
// 解析图片 
|
|
595
|
+
const imageMatch = this.parseImage(remaining.slice(nearest.pos));
|
|
596
|
+
if (imageMatch) {
|
|
597
|
+
parts.push({
|
|
598
|
+
type: 'image',
|
|
599
|
+
alt: imageMatch.alt,
|
|
600
|
+
src: imageMatch.src,
|
|
601
|
+
bold: false
|
|
602
|
+
});
|
|
603
|
+
remaining = remaining.slice(nearest.pos + imageMatch.length);
|
|
604
|
+
} else {
|
|
605
|
+
// 不是有效图片,当作普通文本
|
|
606
|
+
parts.push({ type: 'text', text: '!', bold: false });
|
|
607
|
+
remaining = remaining.slice(nearest.pos + 1);
|
|
608
|
+
}
|
|
609
|
+
} else if (nearest.type === 'link') {
|
|
610
|
+
// 解析链接 [text](url)
|
|
611
|
+
// 需要排除图片的情况(![...)
|
|
612
|
+
if (remaining.charAt(nearest.pos) === '[' && nearest.pos > 0 && remaining.charAt(nearest.pos - 1) === '!') {
|
|
613
|
+
// 这是图片的一部分,跳过
|
|
614
|
+
parts.push({ type: 'text', text: '[', bold: false });
|
|
615
|
+
remaining = remaining.slice(nearest.pos + 1);
|
|
616
|
+
} else {
|
|
617
|
+
const linkMatch = this.parseLink(remaining.slice(nearest.pos));
|
|
618
|
+
if (linkMatch) {
|
|
619
|
+
parts.push({
|
|
620
|
+
type: 'link',
|
|
621
|
+
text: linkMatch.text,
|
|
622
|
+
url: linkMatch.url,
|
|
623
|
+
bold: false
|
|
624
|
+
});
|
|
625
|
+
remaining = remaining.slice(nearest.pos + linkMatch.length);
|
|
626
|
+
} else {
|
|
627
|
+
// 不是有效链接,当作普通文本
|
|
628
|
+
parts.push({ type: 'text', text: '[', bold: false });
|
|
629
|
+
remaining = remaining.slice(nearest.pos + 1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} else if (nearest.type === 'bold' || nearest.type === 'bold2') {
|
|
633
|
+
const marker = nearest.type === 'bold' ? '**' : '__';
|
|
634
|
+
const endPos = remaining.indexOf(marker, nearest.pos + 2);
|
|
635
|
+
if (endPos !== -1) {
|
|
636
|
+
const boldText = remaining.slice(nearest.pos + 2, endPos);
|
|
637
|
+
parts.push({ type: 'text', text: boldText, bold: true });
|
|
638
|
+
remaining = remaining.slice(endPos + 2);
|
|
639
|
+
} else {
|
|
640
|
+
// 没有闭合,当作普通文本
|
|
641
|
+
parts.push({ type: 'text', text: marker, bold: false });
|
|
642
|
+
remaining = remaining.slice(nearest.pos + 2);
|
|
643
|
+
}
|
|
644
|
+
} else if (nearest.type === 'italic' || nearest.type === 'italic2') {
|
|
645
|
+
// 斜体:单个 * 或 _
|
|
646
|
+
const marker = nearest.type === 'italic' ? '*' : '_';
|
|
647
|
+
// 查找闭合的斜体标记(同样需要是单独的 * 或 _)
|
|
648
|
+
let endPos = -1;
|
|
649
|
+
for (let i = nearest.pos + 1; i < remaining.length; i++) {
|
|
650
|
+
if (remaining[i] === marker) {
|
|
651
|
+
// 确保不是 ** 或 __ 的一部分
|
|
652
|
+
if (remaining[i + 1] !== marker && remaining[i - 1] !== marker) {
|
|
653
|
+
endPos = i;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (endPos !== -1) {
|
|
659
|
+
const italicText = remaining.slice(nearest.pos + 1, endPos);
|
|
660
|
+
parts.push({ type: 'text', text: italicText, italic: true, bold: false });
|
|
661
|
+
remaining = remaining.slice(endPos + 1);
|
|
662
|
+
} else {
|
|
663
|
+
// 没有闭合,当作普通文本
|
|
664
|
+
parts.push({ type: 'text', text: marker, bold: false });
|
|
665
|
+
remaining = remaining.slice(nearest.pos + 1);
|
|
666
|
+
}
|
|
667
|
+
} else if (nearest.type === 'code') {
|
|
668
|
+
const endPos = remaining.indexOf('`', nearest.pos + 1);
|
|
669
|
+
if (endPos !== -1) {
|
|
670
|
+
const codeText = remaining.slice(nearest.pos + 1, endPos);
|
|
671
|
+
parts.push({ type: 'code', text: codeText, bold: false });
|
|
672
|
+
remaining = remaining.slice(endPos + 1);
|
|
673
|
+
} else {
|
|
674
|
+
// 没有闭合,当作普通文本
|
|
675
|
+
parts.push({ type: 'text', text: '`', bold: false });
|
|
676
|
+
remaining = remaining.slice(nearest.pos + 1);
|
|
677
|
+
}
|
|
678
|
+
} else if (nearest.type === 'htmlLink') {
|
|
679
|
+
// 解析 HTML <a> 标签
|
|
680
|
+
const htmlLinkMatch = this.parseHtmlLink(remaining.slice(nearest.pos));
|
|
681
|
+
if (htmlLinkMatch) {
|
|
682
|
+
parts.push({
|
|
683
|
+
type: 'link',
|
|
684
|
+
text: htmlLinkMatch.text,
|
|
685
|
+
url: htmlLinkMatch.url,
|
|
686
|
+
bold: false
|
|
687
|
+
});
|
|
688
|
+
remaining = remaining.slice(nearest.pos + htmlLinkMatch.length);
|
|
689
|
+
} else {
|
|
690
|
+
// 不是有效的 HTML 链接,当作普通文本
|
|
691
|
+
parts.push({ type: 'text', text: '<a ', bold: false });
|
|
692
|
+
remaining = remaining.slice(nearest.pos + 3);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return parts.length > 0 ? parts : [{ type: 'text', text: text, bold: false }];
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* 解析链接 [text](url)
|
|
702
|
+
* 返回 { text, url, length } 或 null
|
|
703
|
+
*/
|
|
704
|
+
parseLink(str) {
|
|
705
|
+
if (!str.startsWith('[')) return null;
|
|
706
|
+
|
|
707
|
+
// 找到 ]( 的位置
|
|
708
|
+
const closeBracketIdx = str.indexOf('](');
|
|
709
|
+
if (closeBracketIdx === -1) return null;
|
|
710
|
+
|
|
711
|
+
const linkText = str.slice(1, closeBracketIdx);
|
|
712
|
+
|
|
713
|
+
// 从 ]( 后面开始找匹配的 )
|
|
714
|
+
// 需要处理 URL 中可能包含的括号
|
|
715
|
+
let parenCount = 0;
|
|
716
|
+
let urlEnd = -1;
|
|
717
|
+
for (let i = closeBracketIdx + 2; i < str.length; i++) {
|
|
718
|
+
if (str[i] === '(') parenCount++;
|
|
719
|
+
else if (str[i] === ')') {
|
|
720
|
+
if (parenCount === 0) {
|
|
721
|
+
urlEnd = i;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
parenCount--;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (urlEnd === -1) return null;
|
|
729
|
+
|
|
730
|
+
const url = str.slice(closeBracketIdx + 2, urlEnd);
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
text: linkText,
|
|
734
|
+
url: url,
|
|
735
|
+
length: urlEnd + 1
|
|
736
|
+
};
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 解析图片 
|
|
741
|
+
* 返回 { alt, src, length } 或 null
|
|
742
|
+
*/
|
|
743
|
+
parseImage(str) {
|
|
744
|
+
if (!str.startsWith(';
|
|
748
|
+
if (closeBracketIdx === -1) return null;
|
|
749
|
+
|
|
750
|
+
const altText = str.slice(2, closeBracketIdx);
|
|
751
|
+
|
|
752
|
+
// 从 ]( 后面开始找匹配的 )
|
|
753
|
+
let parenCount = 0;
|
|
754
|
+
let urlEnd = -1;
|
|
755
|
+
for (let i = closeBracketIdx + 2; i < str.length; i++) {
|
|
756
|
+
if (str[i] === '(') parenCount++;
|
|
757
|
+
else if (str[i] === ')') {
|
|
758
|
+
if (parenCount === 0) {
|
|
759
|
+
urlEnd = i;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
parenCount--;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (urlEnd === -1) return null;
|
|
767
|
+
|
|
768
|
+
const src = str.slice(closeBracketIdx + 2, urlEnd);
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
alt: altText,
|
|
772
|
+
src: src,
|
|
773
|
+
length: urlEnd + 1
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* 解析 HTML <a> 标签
|
|
779
|
+
* 支持格式: <a href="url">text</a> 或 <a href="url" >text</a>
|
|
780
|
+
* 返回 { text, url, length } 或 null
|
|
781
|
+
*/
|
|
782
|
+
parseHtmlLink(str) {
|
|
783
|
+
if (!str.startsWith('<a ')) return null;
|
|
784
|
+
|
|
785
|
+
// 查找 href 属性
|
|
786
|
+
// 支持 href="..." 或 href='...'
|
|
787
|
+
const hrefMatch = str.match(/<a\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/i);
|
|
788
|
+
if (!hrefMatch) return null;
|
|
789
|
+
|
|
790
|
+
const url = hrefMatch[1];
|
|
791
|
+
const openTagEnd = str.indexOf('>', hrefMatch.index || 0);
|
|
792
|
+
if (openTagEnd === -1) return null;
|
|
793
|
+
|
|
794
|
+
// 查找 </a> 结束标签
|
|
795
|
+
const closeTagStart = str.indexOf('</a>', openTagEnd);
|
|
796
|
+
if (closeTagStart === -1) return null;
|
|
797
|
+
|
|
798
|
+
// 提取链接文本
|
|
799
|
+
const linkText = str.slice(openTagEnd + 1, closeTagStart);
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
text: linkText,
|
|
803
|
+
url: url,
|
|
804
|
+
length: closeTagStart + 4 // 包含 </a> 的长度
|
|
805
|
+
};
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* 预处理链接:修复跨行的链接格式
|
|
810
|
+
*/
|
|
811
|
+
preprocessLinks(content) {
|
|
812
|
+
// 处理字面的 \n 字符串(反斜杠+n)转换为实际换行符
|
|
813
|
+
// 这在某些 API 返回的数据中会出现
|
|
814
|
+
let result = content.replace(/\\n/g, '\n');
|
|
815
|
+
|
|
816
|
+
// 修复 [text]\n(url) 或 [text]\r\n(url) 格式
|
|
817
|
+
result = result.replace(/\]\s*[\r\n]+\s*\(/g, '](');
|
|
818
|
+
|
|
819
|
+
// 处理 URL 内部的换行
|
|
820
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]*)\)/g, (match, text, url) => {
|
|
821
|
+
const cleanUrl = url.replace(/[\r\n\s]+/g, '');
|
|
822
|
+
return `[${text}](${cleanUrl})`;
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
return result;
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* 切换思考过程显示
|
|
830
|
+
*/
|
|
831
|
+
toggleThinking() {
|
|
832
|
+
this.setData({
|
|
833
|
+
showThinking: !this.data.showThinking
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* 预览图片
|
|
839
|
+
*/
|
|
840
|
+
onPreviewImage(e) {
|
|
841
|
+
const url = e.currentTarget.dataset.url;
|
|
842
|
+
wx.previewImage({
|
|
843
|
+
current: url,
|
|
844
|
+
urls: [url]
|
|
845
|
+
});
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* 图片加载完成
|
|
850
|
+
* 通知父组件进行滚动调整
|
|
851
|
+
*/
|
|
852
|
+
onImageLoad(e) {
|
|
853
|
+
this.triggerEvent('imageload', { url: e.currentTarget.dataset.url });
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* 打开文件
|
|
858
|
+
*/
|
|
859
|
+
onOpenFile(e) {
|
|
860
|
+
const file = e.currentTarget.dataset.file;
|
|
861
|
+
wx.showToast({
|
|
862
|
+
title: '暂不支持打开文件',
|
|
863
|
+
icon: 'none'
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* 打开链接
|
|
869
|
+
*/
|
|
870
|
+
onOpenLink(e) {
|
|
871
|
+
const url = e.currentTarget.dataset.url;
|
|
872
|
+
|
|
873
|
+
// 判断是否是文档类型(PDF、Word、Excel等)
|
|
874
|
+
const docExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
|
|
875
|
+
const isDocument = docExtensions.some(ext => url.toLowerCase().includes(ext));
|
|
876
|
+
|
|
877
|
+
if (isDocument) {
|
|
878
|
+
// 下载并打开文档
|
|
879
|
+
wx.showLoading({ title: '正在打开...' });
|
|
880
|
+
wx.downloadFile({
|
|
881
|
+
url: url,
|
|
882
|
+
success: (res) => {
|
|
883
|
+
wx.hideLoading();
|
|
884
|
+
if (res.statusCode === 200) {
|
|
885
|
+
wx.openDocument({
|
|
886
|
+
filePath: res.tempFilePath,
|
|
887
|
+
showMenu: true,
|
|
888
|
+
fail: (err) => {
|
|
889
|
+
console.error('打开文档失败:', err);
|
|
890
|
+
this.copyLinkFallback(url);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
} else {
|
|
894
|
+
this.copyLinkFallback(url);
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
fail: (err) => {
|
|
898
|
+
wx.hideLoading();
|
|
899
|
+
console.error('下载文档失败:', err);
|
|
900
|
+
this.copyLinkFallback(url);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
} else {
|
|
904
|
+
// 非文档链接,复制到剪贴板
|
|
905
|
+
this.copyLinkFallback(url);
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* 复制链接(降级方案)
|
|
911
|
+
*/
|
|
912
|
+
copyLinkFallback(url) {
|
|
913
|
+
wx.setClipboardData({
|
|
914
|
+
data: url,
|
|
915
|
+
success: () => {
|
|
916
|
+
wx.showToast({
|
|
917
|
+
title: '链接已复制',
|
|
918
|
+
icon: 'success'
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* 复制代码
|
|
926
|
+
*/
|
|
927
|
+
onCopyCode(e) {
|
|
928
|
+
const code = e.currentTarget.dataset.code;
|
|
929
|
+
wx.setClipboardData({
|
|
930
|
+
data: code,
|
|
931
|
+
success: () => {
|
|
932
|
+
wx.showToast({
|
|
933
|
+
title: '代码已复制',
|
|
934
|
+
icon: 'success'
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
},
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* 复制消息内容
|
|
942
|
+
*/
|
|
943
|
+
onCopy() {
|
|
944
|
+
wx.setClipboardData({
|
|
945
|
+
data: this.properties.message.content,
|
|
946
|
+
success: () => {
|
|
947
|
+
wx.showToast({
|
|
948
|
+
title: '已复制',
|
|
949
|
+
icon: 'success'
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* 播放语音
|
|
957
|
+
*/
|
|
958
|
+
onPlayVoice(e) {
|
|
959
|
+
const id = e.currentTarget.dataset.id;
|
|
960
|
+
const message = this.properties.message;
|
|
961
|
+
this.triggerEvent('playvoice', { id, content: message.content });
|
|
962
|
+
},
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* 重新生成
|
|
966
|
+
*/
|
|
967
|
+
onRegenerate() {
|
|
968
|
+
this.triggerEvent('regenerate', { message: this.properties.message });
|
|
969
|
+
},
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* 编辑消息
|
|
973
|
+
*/
|
|
974
|
+
onEdit() {
|
|
975
|
+
this.triggerEvent('edit', { message: this.properties.message });
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* 点击快捷回复
|
|
980
|
+
*/
|
|
981
|
+
onQuickReply(e) {
|
|
982
|
+
const option = e.currentTarget.dataset.option;
|
|
983
|
+
if (option) {
|
|
984
|
+
this.triggerEvent('quickreply', { content: option });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
});
|