im-ui-mobile 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/components/im-avatar/im-avatar.vue +7 -7
  2. package/components/im-badge/im-badge.vue +326 -0
  3. package/components/im-button/im-button.vue +71 -34
  4. package/components/im-card/im-card.vue +563 -0
  5. package/components/im-chat-item/im-chat-item.vue +5 -4
  6. package/components/im-col/im-col.vue +191 -0
  7. package/components/im-dialog/im-dialog.vue +543 -0
  8. package/components/im-double-tap-view/im-double-tap-view.vue +93 -0
  9. package/components/im-emoji-picker/im-emoji-picker.vue +1143 -0
  10. package/components/im-friend-item/im-friend-item.vue +1 -1
  11. package/components/im-group-item/im-group-item.vue +1 -1
  12. package/components/im-group-member-selector/im-group-member-selector.vue +5 -5
  13. package/components/im-group-rtc-join/im-group-rtc-join.vue +8 -8
  14. package/components/im-icon/im-icon.vue +593 -0
  15. package/components/im-image-upload/im-image-upload.vue +0 -2
  16. package/components/im-link/im-link.vue +628 -0
  17. package/components/im-loading/im-loading.vue +13 -4
  18. package/components/im-mention-picker/im-mention-picker.vue +8 -7
  19. package/components/im-message-action/im-message-action.vue +678 -0
  20. package/components/im-message-item/im-message-item.vue +28 -26
  21. package/components/im-message-list/im-message-list.vue +1108 -0
  22. package/components/im-modal/im-modal.vue +373 -0
  23. package/components/im-nav-bar/im-nav-bar.vue +689 -75
  24. package/components/im-parse/im-parse.vue +1054 -0
  25. package/components/im-popup/im-popup.vue +467 -0
  26. package/components/im-read-receipt/im-read-receipt.vue +10 -10
  27. package/components/im-row/im-row.vue +189 -0
  28. package/components/im-search/im-search.vue +762 -0
  29. package/components/im-sku/im-sku.vue +720 -0
  30. package/components/im-sku/utils/helper.ts +182 -0
  31. package/components/im-stepper/im-stepper.vue +585 -0
  32. package/components/im-stepper/utils/helper.ts +167 -0
  33. package/components/im-tabs/im-tabs.vue +1022 -0
  34. package/components/im-tabs/tabs-navigation.vue +489 -0
  35. package/components/im-tabs/utils/helper.ts +181 -0
  36. package/components/im-tabs-tab-pane/im-tabs-tab-pane.vue +145 -0
  37. package/components/im-upload/im-upload.vue +1236 -0
  38. package/components/im-voice-input/im-voice-input.vue +1 -1
  39. package/index.js +3 -5
  40. package/index.scss +19 -0
  41. package/libs/emoji-data.ts +229 -0
  42. package/libs/index.ts +16 -16
  43. package/package.json +1 -2
  44. package/styles/button.scss +33 -33
  45. package/theme.scss +2 -2
  46. package/types/components/badge.d.ts +42 -0
  47. package/types/components/button.d.ts +2 -1
  48. package/types/components/card.d.ts +122 -0
  49. package/types/components/col.d.ts +37 -0
  50. package/types/components/dialog.d.ts +125 -0
  51. package/types/components/double-tap-view.d.ts +31 -0
  52. package/types/components/emoji-picker.d.ts +121 -0
  53. package/types/components/group-rtc-join.d.ts +1 -1
  54. package/types/components/icon.d.ts +77 -0
  55. package/types/components/link.d.ts +55 -0
  56. package/types/components/loading.d.ts +1 -0
  57. package/types/components/message-action.d.ts +96 -0
  58. package/types/components/message-item.d.ts +2 -2
  59. package/types/components/message-list.d.ts +136 -0
  60. package/types/components/modal.d.ts +106 -0
  61. package/types/components/nav-bar.d.ts +125 -0
  62. package/types/components/parse.d.ts +90 -0
  63. package/types/components/popup.d.ts +58 -0
  64. package/types/components/row.d.ts +31 -0
  65. package/types/components/search.d.ts +54 -0
  66. package/types/components/sku.d.ts +195 -0
  67. package/types/components/stepper.d.ts +99 -0
  68. package/types/components/tabs-tab-pane.d.ts +27 -0
  69. package/types/components/tabs.d.ts +117 -0
  70. package/types/components/upload.d.ts +137 -0
  71. package/types/components.d.ts +19 -1
  72. package/types/index.d.ts +38 -1
  73. package/types/libs/index.d.ts +10 -10
  74. package/types/utils/base64.d.ts +5 -0
  75. package/types/utils/dom.d.ts +3 -0
  76. package/types/utils/enums.d.ts +4 -5
  77. package/types/utils/validator.d.ts +74 -0
  78. package/utils/base64.js +18 -0
  79. package/utils/dom.js +353 -1
  80. package/utils/enums.js +4 -5
  81. package/utils/validator.js +230 -0
  82. package/components/im-file-upload/im-file-upload.vue +0 -309
  83. package/plugins/uview-plus.js +0 -29
  84. package/types/components/arrow-bar.d.ts +0 -14
  85. package/types/components/file-upload.d.ts +0 -58
@@ -0,0 +1,1054 @@
1
+ <template>
2
+ <view class="im-parse" :class="customClass" :style="customStyle">
3
+ <!-- 解析模式 -->
4
+ <template v-if="mode === 'html' && content">
5
+ <rich-text v-if="useRichText"
6
+ :id="richTextId"
7
+ :nodes="dom.nodesToHtml(parsedNodes)"
8
+ />
9
+ <view
10
+ v-else
11
+ class="im-parse__html"
12
+ v-html="content"
13
+ @tap="handleHtmlTap"
14
+ />
15
+ </template>
16
+
17
+ <!-- 文本模式 -->
18
+ <template v-else-if="mode === 'text' && content">
19
+ <text
20
+ class="im-parse__text"
21
+ :style="textStyle"
22
+ @tap="handleTextTap"
23
+ >
24
+ {{ processedText }}
25
+ </text>
26
+ </template>
27
+
28
+ <!-- Markdown 模式 -->
29
+ <template v-else-if="mode === 'markdown' && content">
30
+ <view class="im-parse__markdown">
31
+ <template v-for="(node, index) in markdownNodes" :key="index">
32
+ <!-- 标题 -->
33
+ <view
34
+ v-if="node.type === 'heading'"
35
+ class="im-parse__heading"
36
+ :class="`im-parse__heading--${node.depth}`"
37
+ >
38
+ <text>{{ node.text }}</text>
39
+ </view>
40
+
41
+ <!-- 段落 -->
42
+ <view
43
+ v-else-if="node.type === 'paragraph'"
44
+ class="im-parse__paragraph"
45
+ >
46
+ <text>{{ node.text }}</text>
47
+ </view>
48
+
49
+ <!-- 列表 -->
50
+ <view
51
+ v-else-if="node.type === 'list'"
52
+ class="im-parse__list"
53
+ :class="{ 'im-parse__list--ordered': node.ordered }"
54
+ >
55
+ <view
56
+ v-for="(item, itemIndex) in node.items"
57
+ :key="itemIndex"
58
+ class="im-parse__list-item"
59
+ >
60
+ <view class="im-parse__list-marker">
61
+ {{ node.ordered ? `${Number(itemIndex) + 1}.` : '•' }}
62
+ </view>
63
+ <text class="im-parse__list-text">{{ item }}</text>
64
+ </view>
65
+ </view>
66
+
67
+ <!-- 引用 -->
68
+ <view
69
+ v-else-if="node.type === 'blockquote'"
70
+ class="im-parse__blockquote"
71
+ >
72
+ <text>{{ node.text }}</text>
73
+ </view>
74
+
75
+ <!-- 代码块 -->
76
+ <view
77
+ v-else-if="node.type === 'code'"
78
+ class="im-parse__code-block"
79
+ >
80
+ <text class="im-parse__code-text">{{ node.text }}</text>
81
+ </view>
82
+ </template>
83
+ </view>
84
+ </template>
85
+
86
+ <!-- JSON 模式 -->
87
+ <template v-else-if="mode === 'json' && content">
88
+ <view class="im-parse__json">
89
+ <pre class="im-parse__json-pre">{{ formattedJson }}</pre>
90
+ </view>
91
+ </template>
92
+
93
+ <!-- 默认插槽 -->
94
+ <slot v-if="!content" name="default"></slot>
95
+ </view>
96
+ </template>
97
+
98
+ <script setup lang="ts">
99
+ import { ref, computed, watch } from 'vue'
100
+ import type{LinkData,ParseOptions} from "../../types/components/parse";
101
+ import { emoji, dom } from '../../index'
102
+
103
+ // 定义 Props
104
+ interface Props {
105
+ // 内容
106
+ content?: string
107
+ modelValue?: string
108
+
109
+ // 解析选项
110
+ options?: ParseOptions
111
+
112
+ // 解析模式
113
+ mode?: 'html' | 'text' | 'markdown' | 'json'
114
+ encoding?: 'utf-8' | 'base64' | 'url'
115
+
116
+ // 通用配置
117
+ maxLength?: number
118
+ ellipsis?: string
119
+ lines?: number
120
+
121
+ // HTML 配置
122
+ useRichText?: boolean
123
+ space?: 'ensp' | 'emsp' | 'nbsp'
124
+ filterTags?: string[]
125
+ allowTags?: string[]
126
+ allowAttributes?: Record<string, string[]>
127
+ allowDomains?: string[]
128
+
129
+ // 样式配置
130
+ customClass?: string
131
+ customStyle?: string | Record<string, string>
132
+ textStyle?: string | Record<string, string>
133
+
134
+ // 功能配置
135
+ autolink?: boolean
136
+ emoji?: boolean
137
+ highlight?: boolean
138
+ lazyLoad?: boolean
139
+ errorFallback?: string
140
+
141
+ // 事件配置
142
+ preventDefault?: boolean
143
+ }
144
+
145
+ // 定义 Emits
146
+ interface Emits {
147
+ (e: 'update:modelValue', value: string): void
148
+ (e: 'click', event: any): void
149
+ (e:'link-click',link: LinkData, event?: Event):void
150
+ (e: 'linkpress', url: string): void
151
+ (e: 'imageerror', src: string, error: Error): void
152
+ (e: 'imageload', src: string): void
153
+ (e: 'parse-error', error: Error): void
154
+ (e: 'parse-success', result: any): void
155
+ }
156
+
157
+ // 定义 Props 默认值
158
+ const props = withDefaults(defineProps<Props>(), {
159
+ content: '',
160
+ modelValue: '',
161
+
162
+ options: () => ({
163
+ enableLinks: true,
164
+ openExternalInNewWindow: true,
165
+ parseEmail: false,
166
+ parsePhone: false
167
+ }),
168
+
169
+ mode: 'html',
170
+ encoding: 'utf-8',
171
+
172
+ maxLength: 0,
173
+ ellipsis: '...',
174
+ lines: 0,
175
+
176
+ useRichText: true,
177
+ space: 'ensp',
178
+ filterTags: () => ['script', 'style', 'iframe', 'object', 'embed'],
179
+ allowTags: () => [],
180
+ allowAttributes: () => ({}),
181
+ allowDomains: () => [],
182
+
183
+ customClass: '',
184
+ customStyle: () => ({}),
185
+ textStyle: () => ({}),
186
+
187
+ autolink: true,
188
+ emoji: false,
189
+ highlight: false,
190
+ lazyLoad: false,
191
+ errorFallback: '解析失败',
192
+
193
+ preventDefault: false
194
+ })
195
+
196
+ const emit = defineEmits<Emits>()
197
+
198
+ // 响应式状态
199
+ const richTextId = ref('richTextId'+new Date().getTime())
200
+ const parsedNodes = ref<any[]>([])
201
+ const markdownNodes = ref<any[]>([])
202
+ const lastError = ref<Error | null>(null)
203
+ const extractedLinks = ref<Array<{id: string, data: LinkData}>>([]) // 提取的链接数据
204
+
205
+ // 计算内容
206
+ const computedContent = computed(() => {
207
+ return props.modelValue || props.content
208
+ })
209
+
210
+ // 解码内容
211
+ const decodedContent = computed(() => {
212
+ const content = computedContent.value
213
+ if (!content) return ''
214
+
215
+ try {
216
+ switch (props.encoding) {
217
+ case 'base64':
218
+ return atob(content)
219
+ case 'url':
220
+ return decodeURIComponent(content)
221
+ case 'utf-8':
222
+ default:
223
+ return content
224
+ }
225
+ } catch (error) {
226
+ console.error('解码失败:', error)
227
+ return content
228
+ }
229
+ })
230
+
231
+ // 处理文本内容
232
+ const processedText = computed(() => {
233
+ let text = decodedContent.value
234
+
235
+ // 处理长度限制
236
+ if (props.maxLength > 0 && text.length > props.maxLength) {
237
+ text = text.substring(0, props.maxLength) + props.ellipsis
238
+ }
239
+
240
+ // 自动链接
241
+ if (props.autolink) {
242
+ text = autoLink(text)
243
+ }
244
+
245
+ // 表情符号
246
+ if (props.emoji || (emoji as any).containEmoji(text)) {
247
+ text = parseEmoji(text)
248
+ }
249
+
250
+ // 语法高亮
251
+ if (props.highlight) {
252
+ text = highlightText(text)
253
+ }
254
+
255
+ return text
256
+ })
257
+
258
+ // 生命周期
259
+ onMounted(() => {
260
+ initParse()
261
+ })
262
+
263
+ // 解析 HTML 节点
264
+ const parseHtml = async (html: string) => {
265
+ if (!html) return []
266
+
267
+ try {
268
+ // 简单的 HTML 解析器
269
+ const nodes = parseHtmlToNodes(html)
270
+ emit('parse-success', nodes)
271
+ return nodes
272
+ } catch (error) {
273
+ lastError.value = error as Error
274
+ emit('parse-error', error as Error)
275
+ return [{ type: 'text', text: props.errorFallback }]
276
+ }
277
+ }
278
+
279
+ // 解析 Markdown
280
+ const parseMarkdown = (markdown: string) => {
281
+ if (!markdown) return []
282
+
283
+ try {
284
+ const nodes = parseMarkdownToNodes(markdown)
285
+ emit('parse-success', nodes)
286
+ return nodes
287
+ } catch (error) {
288
+ lastError.value = error as Error
289
+ emit('parse-error', error as Error)
290
+ return [{ type: 'paragraph', text: props.errorFallback }]
291
+ }
292
+ }
293
+
294
+ // 格式化 JSON
295
+ const formattedJson = computed(() => {
296
+ try {
297
+ const content = decodedContent.value
298
+ if (!content) return ''
299
+
300
+ const obj = JSON.parse(content)
301
+ return JSON.stringify(obj, null, 2)
302
+ } catch (error) {
303
+ return props.errorFallback
304
+ }
305
+ })
306
+
307
+ // 简单的 HTML 解析器
308
+ const parseHtmlToNodes = (html: string): any[] => {
309
+ const nodes: any[] = []
310
+
311
+ // 移除危险标签
312
+ let safeHtml = html
313
+ props.filterTags.forEach(tag => {
314
+ const regex = new RegExp(`<${tag}[^>]*>.*?</${tag}>`, 'gis')
315
+ safeHtml = safeHtml.replace(regex, '')
316
+ })
317
+
318
+ // 简化的解析逻辑
319
+ // 实际项目中可以使用更完善的 HTML 解析库
320
+ const textMatch = safeHtml.match(/>(.*?)</)
321
+ if (textMatch) {
322
+ nodes.push({
323
+ type: 'text',
324
+ text: textMatch[1]
325
+ })
326
+ }
327
+
328
+ return nodes
329
+ }
330
+
331
+ // 简单的 Markdown 解析器
332
+ const parseMarkdownToNodes = (markdown: string): any[] => {
333
+ const nodes: any[] = []
334
+ const lines = markdown.split('\n')
335
+
336
+ for (let i = 0; i < lines.length; i++) {
337
+ const line = lines[i].trim()
338
+
339
+ if (!line) continue
340
+
341
+ // 标题
342
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/)
343
+ if (headingMatch) {
344
+ nodes.push({
345
+ type: 'heading',
346
+ depth: headingMatch[1].length,
347
+ text: headingMatch[2]
348
+ })
349
+ continue
350
+ }
351
+
352
+ // 列表项
353
+ const listMatch = line.match(/^[\*\-\+]\s+(.+)$/)
354
+ if (listMatch) {
355
+ // 查找连续的列表项
356
+ const items = [listMatch[1]]
357
+ while (i + 1 < lines.length && lines[i + 1].match(/^[\*\-\+]\s/)) {
358
+ i++
359
+ items.push(lines[i].replace(/^[\*\-\+]\s+/, ''))
360
+ }
361
+
362
+ nodes.push({
363
+ type: 'list',
364
+ ordered: false,
365
+ items
366
+ })
367
+ continue
368
+ }
369
+
370
+ // 数字列表
371
+ const orderedListMatch = line.match(/^\d+\.\s+(.+)$/)
372
+ if (orderedListMatch) {
373
+ const items = [orderedListMatch[1]]
374
+ while (i + 1 < lines.length && lines[i + 1].match(/^\d+\.\s/)) {
375
+ i++
376
+ items.push(lines[i + 1].replace(/^\d+\.\s+/, ''))
377
+ }
378
+
379
+ nodes.push({
380
+ type: 'list',
381
+ ordered: true,
382
+ items
383
+ })
384
+ continue
385
+ }
386
+
387
+ // 引用
388
+ if (line.startsWith('> ')) {
389
+ nodes.push({
390
+ type: 'blockquote',
391
+ text: line.substring(2)
392
+ })
393
+ continue
394
+ }
395
+
396
+ // 代码块
397
+ if (line.startsWith('```')) {
398
+ const language = line.substring(3).trim() || ''
399
+ const codeLines = []
400
+
401
+ while (i + 1 < lines.length && !lines[i + 1].startsWith('```')) {
402
+ i++
403
+ codeLines.push(lines[i])
404
+ }
405
+ i++ // 跳过结束的 ```
406
+
407
+ nodes.push({
408
+ type: 'code',
409
+ language,
410
+ text: codeLines.join('\n')
411
+ })
412
+ continue
413
+ }
414
+
415
+ // 普通段落
416
+ nodes.push({
417
+ type: 'paragraph',
418
+ text: line
419
+ })
420
+ }
421
+
422
+ return nodes
423
+ }
424
+
425
+ // 自动链接
426
+ const autoLink = (text: string): string => {
427
+ const urlRegex = /(https?:\/\/[^\s]+)/g
428
+ return text.replace(urlRegex, '<a href="$1">$1</a>')
429
+ }
430
+
431
+ // 解析表情符号
432
+ const parseEmoji = (text: string): string => {
433
+ const emojiMap: Record<string, string> = {
434
+ ':)': '😊',
435
+ ':(': '😔',
436
+ ':D': '😃',
437
+ ';)': '😉',
438
+ ':P': '😛',
439
+ ':O': '😮',
440
+ ':*': '😘',
441
+ '<3': '❤️'
442
+ }
443
+
444
+ let result = text
445
+ Object.keys(emojiMap).forEach(key => {
446
+ const k = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
447
+ result = result.replace(new RegExp(k, 'g'), emojiMap[k])
448
+ })
449
+
450
+ return result
451
+ }
452
+
453
+ // 文本高亮
454
+ const highlightText = (text: string): string => {
455
+ // 简单的关键词高亮
456
+ const keywords = ['important', 'urgent', 'note', 'warning', 'info']
457
+ let result = text
458
+
459
+ keywords.forEach(keyword => {
460
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi')
461
+ result = result.replace(regex, `<span class="highlight">${keyword}</span>`)
462
+ })
463
+
464
+ return result
465
+ }
466
+
467
+ // 处理链接点击
468
+ const handleLinkPress = (event: any) => {
469
+ const url = event.detail?.href
470
+ if (url) {
471
+ emit('linkpress', url)
472
+
473
+ if (props.preventDefault) {
474
+ return
475
+ }
476
+
477
+ // 检查域名是否允许
478
+ if (props.allowDomains.length > 0) {
479
+ try {
480
+ const domain = new URL(url).hostname
481
+ if (!props.allowDomains.includes(domain)) {
482
+ uni.showToast({
483
+ title: '禁止访问此域名',
484
+ icon: 'none'
485
+ })
486
+ return
487
+ }
488
+ } catch (error) {
489
+ console.error('解析URL失败:', error)
490
+ }
491
+ }
492
+
493
+ // 跳转到链接
494
+ uni.navigateTo({
495
+ url: `/pages/webview/webview?url=${encodeURIComponent(url)}`
496
+ })
497
+ }
498
+ }
499
+
500
+
501
+ /**
502
+ * 解析 HTML 字符串并提取链接
503
+ */
504
+ const parseHtmlWithLinks = (html: string): { nodes: any[], links: LinkData[] } => {
505
+ const links: LinkData[] = []
506
+ let linkIndex = 0
507
+
508
+ // 使用正则表达式匹配 a 标签
509
+ const linkRegex = /<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)\1[^>]*?>(.*?)<\/a>/gi
510
+
511
+ // 替换链接为可点击元素
512
+ const processedHtml = html.replace(linkRegex, (match, quote, url, text) => {
513
+ // 清理 URL
514
+ const cleanUrl = url.trim()
515
+ const cleanText = text.replace(/<[^>]*>/g, '').trim()
516
+
517
+ // 检查是否为外部链接
518
+ const isExternal = /^(https?:)?\/\//i.test(cleanUrl) &&
519
+ !cleanUrl.includes(location.hostname)
520
+
521
+ const linkData: LinkData = {
522
+ url: cleanUrl,
523
+ text: cleanText,
524
+ isExternal
525
+ }
526
+
527
+ links.push(linkData)
528
+
529
+ // 生成唯一的 ID
530
+ const linkId = `link-${linkIndex++}`
531
+
532
+ // 替换为可点击的富文本节点
533
+ return `<span
534
+ data-link-id="${linkId}"
535
+ data-link-url="${encodeURIComponent(cleanUrl)}"
536
+ style="color: #007AFF; text-decoration: underline;"
537
+ class="im-parse-link"
538
+ >${cleanText}</span>`
539
+ })
540
+
541
+ // 解析电子邮件(如果启用)
542
+ let finalHtml = processedHtml
543
+ if (props.options?.parseEmail) {
544
+ const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g
545
+ finalHtml = finalHtml.replace(emailRegex, (email) => {
546
+ const emailId = `email-${linkIndex++}`
547
+ const linkData: LinkData = {
548
+ url: `mailto:${email}`,
549
+ text: email,
550
+ isExternal: false
551
+ }
552
+ links.push(linkData)
553
+
554
+ return `<span
555
+ data-link-id="${emailId}"
556
+ data-link-url="mailto:${email}"
557
+ style="color: #007AFF; text-decoration: underline;"
558
+ class="im-parse-email"
559
+ >${email}</span>`
560
+ })
561
+ }
562
+
563
+ // 解析电话号码(如果启用)
564
+ if (props.options?.parsePhone) {
565
+ const phoneRegex = /(\+?\d[\d\s\-\(\)]{7,}\d)/g
566
+ finalHtml = finalHtml.replace(phoneRegex, (phone) => {
567
+ const cleanPhone = phone.replace(/[\s\-\(\)]/g, '')
568
+ const phoneId = `phone-${linkIndex++}`
569
+ const linkData: LinkData = {
570
+ url: `tel:${cleanPhone}`,
571
+ text: phone,
572
+ isExternal: false
573
+ }
574
+ links.push(linkData)
575
+
576
+ return `<span
577
+ data-link-id="${phoneId}"
578
+ data-link-url="tel:${cleanPhone}"
579
+ style="color: #007AFF; text-decoration: underline;"
580
+ class="im-parse-phone"
581
+ >${phone}</span>`
582
+ })
583
+ }
584
+
585
+ // 提取链接信息到响应式数组
586
+ extractedLinks.value = links.map((link, index) => ({
587
+ id: `link-${index}`,
588
+ data: link
589
+ }))
590
+
591
+ // 返回解析后的节点(这里简化处理,实际可能需要更复杂的 HTML 解析)
592
+ return {
593
+ nodes: [{
594
+ name: 'div',
595
+ attrs: {
596
+ class: 'im-parse-content'
597
+ },
598
+ children: [{
599
+ type: 'text',
600
+ text: finalHtml
601
+ }]
602
+ }],
603
+ links
604
+ }
605
+ }
606
+
607
+ /**
608
+ * 处理链接点击
609
+ */
610
+ const handleLinkClick = (linkData: LinkData, event?: Event) => {
611
+ // 触发自定义事件
612
+ emit('link-click', linkData, event)
613
+
614
+ // 调用用户提供的回调
615
+ if (props.options?.onLinkClick) {
616
+ props.options.onLinkClick(linkData, event)
617
+ }
618
+
619
+ // 执行默认行为(在 uniapp 中)
620
+ handleDefaultLinkBehavior(linkData)
621
+ }
622
+
623
+ /**
624
+ * 处理默认链接行为
625
+ */
626
+ const handleDefaultLinkBehavior = (linkData: LinkData) => {
627
+ const { url, isExternal } = linkData
628
+
629
+ // 处理邮件链接
630
+ if (url.startsWith('mailto:')) {
631
+ uni.makePhoneCall({
632
+ phoneNumber: url.replace('mailto:', ''),
633
+ fail: () => {
634
+ uni.showToast({
635
+ title: '无法打开邮件客户端',
636
+ icon: 'none'
637
+ })
638
+ }
639
+ })
640
+ return
641
+ }
642
+
643
+ // 处理电话链接
644
+ if (url.startsWith('tel:')) {
645
+ uni.makePhoneCall({
646
+ phoneNumber: url.replace('tel:', ''),
647
+ fail: () => {
648
+ uni.showToast({
649
+ title: '无法拨打电话',
650
+ icon: 'none'
651
+ })
652
+ }
653
+ })
654
+ return
655
+ }
656
+
657
+ // 处理网页链接
658
+ if (/^(https?:)?\/\//i.test(url)) {
659
+ // 在 uniapp 中打开网页
660
+ uni.navigateTo({
661
+ url: `/pages/webview/webview?url=${encodeURIComponent(url)}`,
662
+ fail: () => {
663
+ // 如果页面不存在,尝试使用浏览器打开
664
+ if (isExternal && props.options?.openExternalInNewWindow) {
665
+ plus.runtime.openURL(url, (err) => {
666
+ uni.showToast({
667
+ title: '无法打开链接',
668
+ icon: 'none'
669
+ })
670
+ })
671
+ }
672
+ }
673
+ })
674
+ } else {
675
+ // 内部链接
676
+ uni.navigateTo({
677
+ url: url,
678
+ fail: () => {
679
+ uni.showToast({
680
+ title: '页面不存在',
681
+ icon: 'none'
682
+ })
683
+ }
684
+ })
685
+ }
686
+ }
687
+
688
+ /**
689
+ * 处理富文本内部点击事件
690
+ */
691
+ const handleRichTextItemClick = (event: any) => {
692
+ console.log('click', event)
693
+ const { dataset } = event.detail || {}
694
+
695
+ if (dataset && dataset.linkId) {
696
+ const linkItem = extractedLinks.value.find(item => item.id === dataset.linkId)
697
+ if (linkItem) {
698
+ // 解码 URL
699
+ const decodedUrl = decodeURIComponent(dataset.linkUrl || linkItem.data.url)
700
+ const linkData = {
701
+ ...linkItem.data,
702
+ url: decodedUrl
703
+ }
704
+
705
+ handleLinkClick(linkData, event)
706
+ }
707
+ }
708
+ }
709
+
710
+ /**
711
+ * 初始化解析
712
+ */
713
+ const initParse = () => {
714
+ if (!props.content) {
715
+ parsedNodes.value = []
716
+ return
717
+ }
718
+
719
+ try {
720
+ const result = parseHtmlWithLinks(props.content)
721
+ parsedNodes.value = result.nodes
722
+ } catch (error) {
723
+ console.error('解析内容失败:', error)
724
+ // 出错时显示原始文本
725
+ parsedNodes.value = [{
726
+ name: 'div',
727
+ attrs: {
728
+ class: 'im-parse-error'
729
+ },
730
+ children: [{
731
+ type: 'text',
732
+ text: props.content
733
+ }]
734
+ }]
735
+ }
736
+ }
737
+
738
+ // 处理 HTML 点击
739
+ const handleHtmlTap = (event: any) => {
740
+ emit('click', event)
741
+ }
742
+
743
+ // 处理文本点击
744
+ const handleTextTap = (event: any) => {
745
+ emit('click', event)
746
+ }
747
+
748
+ // 图片加载错误
749
+ const handleImageError = (src: string, error: Error) => {
750
+ emit('imageerror', src, error)
751
+ }
752
+
753
+ // 图片加载成功
754
+ const handleImageLoad = (src: string) => {
755
+ emit('imageload', src)
756
+ }
757
+
758
+ // 重新解析
759
+ const reparse = async () => {
760
+ lastError.value = null
761
+
762
+ if (props.mode === 'html') {
763
+ // parsedNodes.value = await parseHtml(decodedContent.value)
764
+ initParse()
765
+ } else if (props.mode === 'markdown') {
766
+ markdownNodes.value = parseMarkdown(decodedContent.value)
767
+ }
768
+ }
769
+
770
+ // 获取解析结果
771
+ const getParsedResult = () => {
772
+ if (props.mode === 'html') {
773
+ return parsedNodes.value
774
+ } else if (props.mode === 'markdown') {
775
+ return markdownNodes.value
776
+ } else if (props.mode === 'json') {
777
+ try {
778
+ return JSON.parse(decodedContent.value)
779
+ } catch {
780
+ return null
781
+ }
782
+ }
783
+ return processedText.value
784
+ }
785
+
786
+ // 清空内容
787
+ const clear = () => {
788
+ parsedNodes.value = []
789
+ markdownNodes.value = []
790
+ lastError.value = null
791
+ emit('update:modelValue', '')
792
+ }
793
+
794
+
795
+ // 监听内容变化
796
+ watch(() => computedContent.value, async (newContent) => {
797
+ if (newContent) {
798
+ await reparse()
799
+ } else {
800
+ clear()
801
+ }
802
+ }, { immediate: true })
803
+
804
+ watch(() => props.content, () => {
805
+ initParse()
806
+ })
807
+
808
+ watch(() => props.options, () => {
809
+ initParse()
810
+ }, { deep: true })
811
+
812
+ // 暴露方法
813
+ defineExpose({
814
+ reparse,
815
+ getParsedResult,
816
+ clear,
817
+ getError: () => lastError.value
818
+ })
819
+ </script>
820
+
821
+ <style lang="scss" scoped>
822
+ .im-parse {
823
+ width: 100%;
824
+ word-break: break-word;
825
+ line-height: 1.6;
826
+
827
+ &__html {
828
+ // HTML 样式
829
+ :deep(img) {
830
+ max-width: 100%;
831
+ height: auto;
832
+ vertical-align: middle;
833
+ }
834
+
835
+ :deep(a) {
836
+ color: #409eff;
837
+ text-decoration: none;
838
+
839
+ &:active {
840
+ opacity: 0.7;
841
+ }
842
+ }
843
+
844
+ :deep(p) {
845
+ margin: 16rpx 0;
846
+ }
847
+
848
+ :deep(h1),
849
+ :deep(h2),
850
+ :deep(h3),
851
+ :deep(h4),
852
+ :deep(h5),
853
+ :deep(h6) {
854
+ margin: 32rpx 0 16rpx;
855
+ font-weight: bold;
856
+ }
857
+
858
+ :deep(h1) {
859
+ font-size: 40rpx;
860
+ }
861
+
862
+ :deep(h2) {
863
+ font-size: 36rpx;
864
+ }
865
+
866
+ :deep(h3) {
867
+ font-size: 32rpx;
868
+ }
869
+
870
+ :deep(h4) {
871
+ font-size: 28rpx;
872
+ }
873
+
874
+ :deep(ul),
875
+ :deep(ol) {
876
+ margin: 16rpx 0;
877
+ padding-left: 48rpx;
878
+ }
879
+
880
+ :deep(li) {
881
+ margin: 8rpx 0;
882
+ }
883
+
884
+ :deep(blockquote) {
885
+ margin: 16rpx 0;
886
+ padding: 16rpx 32rpx;
887
+ background-color: #f5f5f5;
888
+ border-left: 8rpx solid #ddd;
889
+ color: #666;
890
+ }
891
+
892
+ :deep(code) {
893
+ padding: 4rpx 8rpx;
894
+ background-color: #f5f5f5;
895
+ border-radius: 4rpx;
896
+ font-family: Consolas, Monaco, 'Courier New', monospace;
897
+ font-size: 90%;
898
+ }
899
+
900
+ :deep(pre) {
901
+ margin: 16rpx 0;
902
+ padding: 24rpx;
903
+ background-color: #f5f5f5;
904
+ border-radius: 8rpx;
905
+ overflow-x: auto;
906
+
907
+ code {
908
+ padding: 0;
909
+ background-color: transparent;
910
+ }
911
+ }
912
+
913
+ :deep(table) {
914
+ width: 100%;
915
+ border-collapse: collapse;
916
+ margin: 16rpx 0;
917
+
918
+ th,
919
+ td {
920
+ padding: 16rpx;
921
+ border: 2rpx solid #ddd;
922
+ text-align: left;
923
+ }
924
+
925
+ th {
926
+ background-color: #f5f5f5;
927
+ font-weight: bold;
928
+ }
929
+ }
930
+ }
931
+
932
+ &__text {
933
+ display: block;
934
+ width: 100%;
935
+ }
936
+
937
+ &__markdown {
938
+ .highlight {
939
+ background-color: #fffbe6;
940
+ padding: 2rpx 6rpx;
941
+ border-radius: 4rpx;
942
+ color: #d48806;
943
+ }
944
+ }
945
+
946
+ &__heading {
947
+ margin: 32rpx 0 16rpx;
948
+ font-weight: bold;
949
+
950
+ &--1 {
951
+ font-size: 40rpx;
952
+ }
953
+
954
+ &--2 {
955
+ font-size: 36rpx;
956
+ }
957
+
958
+ &--3 {
959
+ font-size: 32rpx;
960
+ }
961
+
962
+ &--4 {
963
+ font-size: 28rpx;
964
+ }
965
+
966
+ &--5 {
967
+ font-size: 26rpx;
968
+ }
969
+
970
+ &--6 {
971
+ font-size: 24rpx;
972
+ }
973
+ }
974
+
975
+ &__paragraph {
976
+ margin: 16rpx 0;
977
+ }
978
+
979
+ &__list {
980
+ margin: 16rpx 0;
981
+
982
+ &--ordered {
983
+ padding-left: 48rpx;
984
+ list-style-type: decimal;
985
+ }
986
+
987
+ &:not(.im-parse__list--ordered) {
988
+ padding-left: 48rpx;
989
+ list-style-type: disc;
990
+ }
991
+ }
992
+
993
+ &__list-item {
994
+ display: flex;
995
+ align-items: flex-start;
996
+ margin: 8rpx 0;
997
+ }
998
+
999
+ &__list-marker {
1000
+ margin-right: 16rpx;
1001
+ min-width: 32rpx;
1002
+ text-align: right;
1003
+ }
1004
+
1005
+ &__list-text {
1006
+ flex: 1;
1007
+ }
1008
+
1009
+ &__blockquote {
1010
+ margin: 16rpx 0;
1011
+ padding: 16rpx 32rpx;
1012
+ background-color: #f5f5f5;
1013
+ border-left: 8rpx solid #ddd;
1014
+ color: #666;
1015
+ }
1016
+
1017
+ &__code-block {
1018
+ margin: 16rpx 0;
1019
+ padding: 24rpx;
1020
+ background-color: #f5f5f5;
1021
+ border-radius: 8rpx;
1022
+ overflow-x: auto;
1023
+ }
1024
+
1025
+ &__code-text {
1026
+ font-family: Consolas, Monaco, 'Courier New', monospace;
1027
+ font-size: 90%;
1028
+ white-space: pre;
1029
+ }
1030
+
1031
+ &__json {
1032
+ width: 100%;
1033
+ overflow-x: auto;
1034
+ }
1035
+
1036
+ &__json-pre {
1037
+ margin: 0;
1038
+ padding: 24rpx;
1039
+ background-color: #f5f5f5;
1040
+ border-radius: 8rpx;
1041
+ font-family: Consolas, Monaco, 'Courier New', monospace;
1042
+ font-size: 90%;
1043
+ white-space: pre;
1044
+ }
1045
+ }
1046
+
1047
+ // 多行省略
1048
+ .im-parse--ellipsis {
1049
+ display: -webkit-box;
1050
+ -webkit-box-orient: vertical;
1051
+ overflow: hidden;
1052
+ text-overflow: ellipsis;
1053
+ }
1054
+ </style>