fix(RichTextRenderer): 重写富文本渲染逻辑修复兼容问题
替换原有的v-html渲染方案,移除过时的transformElement与事件绑定逻辑,使用htmlparser2解析HTML为rich-text组件支持的节点格式,统一配置标签样式,添加图片预览、链接点击与长按复制等交互逻辑,彻底解决原有实现的各类兼容问题。
Showing
1 changed file
with
292 additions
and
292 deletions
| 1 | <template> | 1 | <template> |
| 2 | - <view :id="containerId" class="rich-text-renderer" v-html="processedContent"></view> | 2 | + <rich-text |
| 3 | + class="rich-text-renderer" | ||
| 4 | + :nodes="richTextNodes" | ||
| 5 | + space="nbsp" | ||
| 6 | + :user-select="true" | ||
| 7 | + @tap="handleRichTextTap" | ||
| 8 | + @longtap="handleRichTextLongTap" | ||
| 9 | + /> | ||
| 3 | </template> | 10 | </template> |
| 4 | 11 | ||
| 5 | <script setup> | 12 | <script setup> |
| 6 | -import { ref, watch, nextTick, onBeforeUnmount } from 'vue' | 13 | +import { computed } from 'vue' |
| 7 | import Taro from '@tarojs/taro' | 14 | import Taro from '@tarojs/taro' |
| 8 | -import { $ } from '@tarojs/extend' | 15 | +import { parseDocument } from 'htmlparser2' |
| 9 | 16 | ||
| 10 | const props = defineProps({ | 17 | const props = defineProps({ |
| 11 | content: { | 18 | content: { |
| 12 | type: String, | 19 | type: String, |
| 13 | default: '', | 20 | default: '', |
| 14 | }, | 21 | }, |
| 15 | - enableTransform: { | ||
| 16 | - type: Boolean, | ||
| 17 | - default: true, | ||
| 18 | - }, | ||
| 19 | }) | 22 | }) |
| 20 | 23 | ||
| 21 | const emit = defineEmits(['image-preview', 'file-click', 'link-copy']) | 24 | const emit = defineEmits(['image-preview', 'file-click', 'link-copy']) |
| 22 | 25 | ||
| 23 | -const processedContent = ref('') | 26 | +const BLOCK_TAGS = new Set([ |
| 24 | -const containerId = `rich-text-renderer-${Math.random().toString(36).slice(-8)}` | 27 | + 'address', |
| 25 | -const containerSelector = `#${containerId}` | 28 | + 'article', |
| 26 | -const previousTransformElement = Taro.options.html?.transformElement | 29 | + 'aside', |
| 30 | + 'blockquote', | ||
| 31 | + 'body', | ||
| 32 | + 'dd', | ||
| 33 | + 'details', | ||
| 34 | + 'div', | ||
| 35 | + 'dl', | ||
| 36 | + 'dt', | ||
| 37 | + 'fieldset', | ||
| 38 | + 'figcaption', | ||
| 39 | + 'figure', | ||
| 40 | + 'footer', | ||
| 41 | + 'form', | ||
| 42 | + 'h1', | ||
| 43 | + 'h2', | ||
| 44 | + 'h3', | ||
| 45 | + 'h4', | ||
| 46 | + 'h5', | ||
| 47 | + 'h6', | ||
| 48 | + 'header', | ||
| 49 | + 'hgroup', | ||
| 50 | + 'hr', | ||
| 51 | + 'li', | ||
| 52 | + 'main', | ||
| 53 | + 'nav', | ||
| 54 | + 'ol', | ||
| 55 | + 'p', | ||
| 56 | + 'pre', | ||
| 57 | + 'section', | ||
| 58 | + 'table', | ||
| 59 | + 'tbody', | ||
| 60 | + 'thead', | ||
| 61 | + 'tfoot', | ||
| 62 | + 'tr', | ||
| 63 | + 'td', | ||
| 64 | + 'th', | ||
| 65 | + 'ul', | ||
| 66 | +]) | ||
| 67 | + | ||
| 68 | +const TEXT_TAGS = new Set([ | ||
| 69 | + 'a', | ||
| 70 | + 'abbr', | ||
| 71 | + 'b', | ||
| 72 | + 'code', | ||
| 73 | + 'del', | ||
| 74 | + 'em', | ||
| 75 | + 'i', | ||
| 76 | + 'mark', | ||
| 77 | + 'q', | ||
| 78 | + 's', | ||
| 79 | + 'small', | ||
| 80 | + 'span', | ||
| 81 | + 'strong', | ||
| 82 | + 'sub', | ||
| 83 | + 'sup', | ||
| 84 | + 'text', | ||
| 85 | + 'u', | ||
| 86 | +]) | ||
| 87 | + | ||
| 88 | +const VOID_TAGS = new Set(['br', 'hr', 'img', 'image']) | ||
| 89 | + | ||
| 90 | +const BASE_TEXT_STYLE = 'color:#4b5563;font-size:30rpx;line-height:1.8;word-break:break-word;' | ||
| 91 | +const PARAGRAPH_STYLE = `${BASE_TEXT_STYLE}display:block;margin:0 0 20rpx;` | ||
| 92 | +const HEADING_STYLE = | ||
| 93 | + 'color:#111827;font-weight:700;line-height:1.5;margin:24rpx 0 16rpx;word-break:break-word;' | ||
| 94 | +const TAG_STYLE_MAP = { | ||
| 95 | + a: 'color:#2563eb;text-decoration:underline;word-break:break-all;', | ||
| 96 | + blockquote: `${PARAGRAPH_STYLE}padding:16rpx 20rpx;border-left:6rpx solid #d1d5db;background:#f9fafb;`, | ||
| 97 | + br: '', | ||
| 98 | + code: 'font-family:monospace;background:#f3f4f6;padding:4rpx 8rpx;border-radius:8rpx;', | ||
| 99 | + div: PARAGRAPH_STYLE, | ||
| 100 | + em: 'font-style:italic;', | ||
| 101 | + h1: `${HEADING_STYLE}font-size:40rpx;`, | ||
| 102 | + h2: `${HEADING_STYLE}font-size:36rpx;`, | ||
| 103 | + h3: `${HEADING_STYLE}font-size:34rpx;`, | ||
| 104 | + h4: `${HEADING_STYLE}font-size:32rpx;`, | ||
| 105 | + h5: `${HEADING_STYLE}font-size:30rpx;`, | ||
| 106 | + h6: `${HEADING_STYLE}font-size:28rpx;`, | ||
| 107 | + hr: 'display:block;height:1rpx;background:#e5e7eb;margin:24rpx 0;', | ||
| 108 | + img: 'width:100%;max-width:100%;height:auto;display:block;margin:24rpx 0;border-radius:16rpx;', | ||
| 109 | + image: 'width:100%;max-width:100%;height:auto;display:block;margin:24rpx 0;border-radius:16rpx;', | ||
| 110 | + li: `${BASE_TEXT_STYLE}display:list-item;margin:0 0 12rpx;`, | ||
| 111 | + ol: `${BASE_TEXT_STYLE}display:block;margin:0 0 20rpx;padding-left:36rpx;list-style-type:decimal;`, | ||
| 112 | + p: PARAGRAPH_STYLE, | ||
| 113 | + pre: `${PARAGRAPH_STYLE}white-space:pre-wrap;background:#f9fafb;padding:20rpx;border-radius:16rpx;overflow:hidden;`, | ||
| 114 | + s: 'text-decoration:line-through;', | ||
| 115 | + strong: 'font-weight:700;', | ||
| 116 | + table: 'display:table;width:100%;border-spacing:0;border-collapse:collapse;margin:20rpx 0;', | ||
| 117 | + td: `${BASE_TEXT_STYLE}display:table-cell;padding:12rpx;border:1rpx solid #d1d5db;vertical-align:top;`, | ||
| 118 | + th: `${BASE_TEXT_STYLE}display:table-cell;padding:12rpx;border:1rpx solid #d1d5db;vertical-align:top;font-weight:700;background:#f9fafb;`, | ||
| 119 | + tr: 'display:table-row;', | ||
| 120 | + u: 'text-decoration:underline;', | ||
| 121 | + ul: `${BASE_TEXT_STYLE}display:block;margin:0 0 20rpx;padding-left:36rpx;list-style-type:disc;`, | ||
| 122 | +} | ||
| 27 | 123 | ||
| 28 | const decodeHtmlEntities = html => { | 124 | const decodeHtmlEntities = html => { |
| 29 | if (!html) { | 125 | if (!html) { |
| 30 | return '' | 126 | return '' |
| 31 | } | 127 | } |
| 32 | 128 | ||
| 33 | - if (process.env.TARO_ENV === 'h5' && typeof document !== 'undefined') { | ||
| 34 | - try { | ||
| 35 | - const textArea = document.createElement('textarea') | ||
| 36 | - textArea.innerHTML = html | ||
| 37 | - const decoded = textArea.value | ||
| 38 | - if (decoded !== html) { | ||
| 39 | - return decoded | ||
| 40 | - } | ||
| 41 | - } catch (error) { | ||
| 42 | - console.warn('[RichTextRenderer] DOM 解码失败,改用映射表', error) | ||
| 43 | - } | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | const entityMap = { | 129 | const entityMap = { |
| 47 | ' ': '\u00A0', | 130 | ' ': '\u00A0', |
| 48 | '&': '&', | 131 | '&': '&', |
| ... | @@ -65,7 +148,6 @@ const decodeHtmlEntities = html => { | ... | @@ -65,7 +148,6 @@ const decodeHtmlEntities = html => { |
| 65 | } | 148 | } |
| 66 | 149 | ||
| 67 | let result = html | 150 | let result = html |
| 68 | - | ||
| 69 | result = result.replace(/&#(\d+);/g, (_match, dec) => String.fromCharCode(dec)) | 151 | result = result.replace(/&#(\d+);/g, (_match, dec) => String.fromCharCode(dec)) |
| 70 | result = result.replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => | 152 | result = result.replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => |
| 71 | String.fromCharCode(parseInt(hex, 16)) | 153 | String.fromCharCode(parseInt(hex, 16)) |
| ... | @@ -78,27 +160,183 @@ const decodeHtmlEntities = html => { | ... | @@ -78,27 +160,183 @@ const decodeHtmlEntities = html => { |
| 78 | return result | 160 | return result |
| 79 | } | 161 | } |
| 80 | 162 | ||
| 81 | -const replaceAnchorTags = html => { | 163 | +const normalizeHtml = raw => { |
| 82 | - let content = html | 164 | + if (!raw) { |
| 83 | - // 小程序 rich-text 对原生 a 标签交互能力有限,统一替换成可绑定事件的块级节点。 | 165 | + return '' |
| 84 | - content = content.replace(/<a\s+/g, '<div class="rich-text-link" ') | 166 | + } |
| 85 | - content = content.replace(/href=/g, 'data-href=') | 167 | + |
| 86 | - content = content.replace(/<\/a>/g, '</div>') | 168 | + let normalized = decodeHtmlEntities(raw) |
| 87 | - return content | 169 | + normalized = normalized.replace(/\r\n/g, '\n') |
| 170 | + normalized = normalized.replace(/<br\s*\/?>\n?/gi, '<br/>') | ||
| 171 | + normalized = normalized.replace(/\n/g, '<br/>') | ||
| 172 | + return normalized | ||
| 88 | } | 173 | } |
| 89 | 174 | ||
| 90 | -const processContent = raw => { | 175 | +const mergeStyleText = (...styles) => { |
| 91 | - if (!raw) { | 176 | + return styles |
| 92 | - processedContent.value = '' | 177 | + .filter(Boolean) |
| 178 | + .map(style => String(style).trim()) | ||
| 179 | + .filter(Boolean) | ||
| 180 | + .join('') | ||
| 181 | +} | ||
| 182 | + | ||
| 183 | +const normalizeTagName = name => { | ||
| 184 | + if (!name) { | ||
| 185 | + return 'span' | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + const lowerName = String(name).toLowerCase() | ||
| 189 | + | ||
| 190 | + if (lowerName === 'img') { | ||
| 191 | + return 'img' | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + if (BLOCK_TAGS.has(lowerName) || TEXT_TAGS.has(lowerName) || VOID_TAGS.has(lowerName)) { | ||
| 195 | + return lowerName | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + return 'div' | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +const createTextNode = text => ({ | ||
| 202 | + type: 'text', | ||
| 203 | + text, | ||
| 204 | +}) | ||
| 205 | + | ||
| 206 | +const mapAttributes = (tagName, attribs = {}) => { | ||
| 207 | + const attrs = {} | ||
| 208 | + | ||
| 209 | + Object.entries(attribs).forEach(([key, value]) => { | ||
| 210 | + if (value === null || value === undefined || value === '') { | ||
| 211 | + return | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + if (key === 'class') { | ||
| 93 | return | 215 | return |
| 94 | } | 216 | } |
| 95 | 217 | ||
| 96 | - let processed = raw | 218 | + if (key === 'href') { |
| 97 | - processed = decodeHtmlEntities(processed) | 219 | + attrs['data-href'] = value |
| 98 | - processed = replaceAnchorTags(processed) | 220 | + return |
| 99 | - processedContent.value = processed | 221 | + } |
| 222 | + | ||
| 223 | + if (key === 'src') { | ||
| 224 | + attrs.src = value | ||
| 225 | + attrs['data-src'] = value | ||
| 226 | + return | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + attrs[key] = value | ||
| 230 | + }) | ||
| 231 | + | ||
| 232 | + const originalStyle = attribs.style || '' | ||
| 233 | + attrs.style = mergeStyleText(TAG_STYLE_MAP[tagName], originalStyle) | ||
| 234 | + | ||
| 235 | + return attrs | ||
| 236 | +} | ||
| 237 | + | ||
| 238 | +const convertDomNode = node => { | ||
| 239 | + if (!node) { | ||
| 240 | + return null | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + if (node.type === 'text') { | ||
| 244 | + if (!node.data) { | ||
| 245 | + return null | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + return createTextNode(node.data) | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + if (node.type === 'script' || node.type === 'style') { | ||
| 252 | + return null | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + if (node.type === 'tag') { | ||
| 256 | + const tagName = normalizeTagName(node.name) | ||
| 257 | + | ||
| 258 | + const children = Array.isArray(node.children) | ||
| 259 | + ? node.children.map(convertDomNode).filter(Boolean) | ||
| 260 | + : [] | ||
| 261 | + | ||
| 262 | + if (tagName === 'br') { | ||
| 263 | + return { | ||
| 264 | + name: 'div', | ||
| 265 | + attrs: { | ||
| 266 | + style: PARAGRAPH_STYLE, | ||
| 267 | + }, | ||
| 268 | + children: [createTextNode('\n')], | ||
| 269 | + } | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + if (tagName === 'hr') { | ||
| 273 | + return { | ||
| 274 | + name: 'div', | ||
| 275 | + attrs: { | ||
| 276 | + style: TAG_STYLE_MAP.hr, | ||
| 277 | + }, | ||
| 278 | + children: [], | ||
| 279 | + } | ||
| 280 | + } | ||
| 281 | + | ||
| 282 | + return { | ||
| 283 | + name: tagName, | ||
| 284 | + attrs: mapAttributes(tagName, node.attribs || {}), | ||
| 285 | + children, | ||
| 286 | + } | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + if (node.type === 'root') { | ||
| 290 | + return { | ||
| 291 | + name: 'div', | ||
| 292 | + attrs: { | ||
| 293 | + style: BASE_TEXT_STYLE, | ||
| 294 | + }, | ||
| 295 | + children: (node.children || []).map(convertDomNode).filter(Boolean), | ||
| 296 | + } | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + return null | ||
| 300 | +} | ||
| 301 | + | ||
| 302 | +const buildNodesFromHtml = html => { | ||
| 303 | + if (!html) { | ||
| 304 | + return [] | ||
| 305 | + } | ||
| 306 | + | ||
| 307 | + try { | ||
| 308 | + const document = parseDocument(html, { | ||
| 309 | + decodeEntities: false, | ||
| 310 | + lowerCaseAttributeNames: false, | ||
| 311 | + lowerCaseTags: true, | ||
| 312 | + recognizeSelfClosing: true, | ||
| 313 | + }) | ||
| 314 | + | ||
| 315 | + return (document.children || []).map(convertDomNode).filter(Boolean) | ||
| 316 | + } catch (error) { | ||
| 317 | + console.error('[RichTextRenderer] 富文本解析失败:', error) | ||
| 318 | + return [ | ||
| 319 | + { | ||
| 320 | + name: 'div', | ||
| 321 | + attrs: { | ||
| 322 | + style: PARAGRAPH_STYLE, | ||
| 323 | + }, | ||
| 324 | + children: [createTextNode(html.replace(/<[^>]+>/g, ''))], | ||
| 325 | + }, | ||
| 326 | + ] | ||
| 327 | + } | ||
| 100 | } | 328 | } |
| 101 | 329 | ||
| 330 | +const richTextNodes = computed(() => { | ||
| 331 | + const normalized = normalizeHtml(props.content) | ||
| 332 | + | ||
| 333 | + if (!normalized) { | ||
| 334 | + return [] | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + return buildNodesFromHtml(normalized) | ||
| 338 | +}) | ||
| 339 | + | ||
| 102 | const isImageUrl = (url = '') => /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(url) | 340 | const isImageUrl = (url = '') => /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(url) |
| 103 | 341 | ||
| 104 | const getFileNameFromUrl = (url = '') => { | 342 | const getFileNameFromUrl = (url = '') => { |
| ... | @@ -137,6 +375,9 @@ const openFileLink = async (url, fileName) => { | ... | @@ -137,6 +375,9 @@ const openFileLink = async (url, fileName) => { |
| 137 | current: url, | 375 | current: url, |
| 138 | indicator: 'default', | 376 | indicator: 'default', |
| 139 | loop: false, | 377 | loop: false, |
| 378 | + success: () => { | ||
| 379 | + emit('image-preview', { src: url }) | ||
| 380 | + }, | ||
| 140 | }) | 381 | }) |
| 141 | return | 382 | return |
| 142 | } | 383 | } |
| ... | @@ -174,276 +415,35 @@ const openFileLink = async (url, fileName) => { | ... | @@ -174,276 +415,35 @@ const openFileLink = async (url, fileName) => { |
| 174 | } | 415 | } |
| 175 | } | 416 | } |
| 176 | 417 | ||
| 177 | -const setupTransformElement = () => { | 418 | +const getNodeUrlFromEvent = event => { |
| 178 | - if (!props.enableTransform) { | 419 | + const dataset = event?.target?.dataset || event?.currentTarget?.dataset || {} |
| 179 | - Taro.options.html.transformElement = previousTransformElement | 420 | + return dataset.href || dataset.src || '' |
| 180 | - return | ||
| 181 | - } | ||
| 182 | - | ||
| 183 | - Taro.options.html.transformElement = element => { | ||
| 184 | - const transformed = previousTransformElement ? previousTransformElement(element) : element | ||
| 185 | - const nodeName = transformed?.nodeName?.toLowerCase() || '' | ||
| 186 | - const tagName = transformed?.tagName?.toLowerCase() || '' | ||
| 187 | - const isImg = | ||
| 188 | - nodeName === 'img' || tagName === 'img' || nodeName === 'image' || tagName === 'image' | ||
| 189 | - | ||
| 190 | - if (!isImg) { | ||
| 191 | - return transformed | ||
| 192 | - } | ||
| 193 | - | ||
| 194 | - // 统一兜底图片样式,避免后端富文本里的宽高写死后撑坏小程序布局。 | ||
| 195 | - if (transformed?.setAttribute) { | ||
| 196 | - transformed.setAttribute('mode', 'widthFix') | ||
| 197 | - transformed.setAttribute('data-rich-image', 'true') | ||
| 198 | - transformed.setAttribute( | ||
| 199 | - 'style', | ||
| 200 | - 'width:100%!important;max-width:100%!important;height:auto!important;display:block;margin:24rpx 0;border-radius:16rpx;' | ||
| 201 | - ) | ||
| 202 | - } | ||
| 203 | - | ||
| 204 | - return transformed | ||
| 205 | - } | ||
| 206 | } | 421 | } |
| 207 | 422 | ||
| 208 | -const bindImageEvents = () => { | 423 | +const handleRichTextTap = async event => { |
| 209 | - nextTick(() => { | 424 | + const url = getNodeUrlFromEvent(event) |
| 210 | - const container = $(containerSelector) | ||
| 211 | - const imgs = container.find('.h5-img') | ||
| 212 | 425 | ||
| 213 | - imgs.forEach(img => { | 426 | + if (!url) { |
| 214 | - const $img = $(img) | ||
| 215 | - $img.off('longpress') | ||
| 216 | - $img.on('longpress', () => { | ||
| 217 | - const src = $img.attr('src') | ||
| 218 | - | ||
| 219 | - if (!src) { | ||
| 220 | return | 427 | return |
| 221 | } | 428 | } |
| 222 | 429 | ||
| 223 | - Taro.previewImage({ | 430 | + const fileName = getFileNameFromUrl(url) |
| 224 | - urls: [src], | 431 | + await openFileLink(url, fileName) |
| 225 | - current: src, | ||
| 226 | - indicator: 'default', | ||
| 227 | - loop: false, | ||
| 228 | - success: () => { | ||
| 229 | - emit('image-preview', { src }) | ||
| 230 | - }, | ||
| 231 | - }) | ||
| 232 | - }) | ||
| 233 | - }) | ||
| 234 | - }) | ||
| 235 | -} | ||
| 236 | - | ||
| 237 | -const bindFileLinkEvents = () => { | ||
| 238 | - nextTick(() => { | ||
| 239 | - const container = $(containerSelector) | ||
| 240 | - const richTextLinks = container.find('.rich-text-link') | ||
| 241 | - const fileLinks = container.find('._file_list') | ||
| 242 | - | ||
| 243 | - let allLinks = [] | ||
| 244 | - if (richTextLinks.length > 0) { | ||
| 245 | - allLinks = allLinks.concat(richTextLinks.toArray()) | ||
| 246 | - } | ||
| 247 | - if (fileLinks.length > 0) { | ||
| 248 | - allLinks = allLinks.concat(fileLinks.toArray()) | ||
| 249 | - } | ||
| 250 | - | ||
| 251 | - allLinks.forEach(el => { | ||
| 252 | - const $el = $(el) | ||
| 253 | - const dataHref = $el.attr('data-href') || $el.attr('href') | ||
| 254 | - | ||
| 255 | - if (!dataHref) { | ||
| 256 | - return | ||
| 257 | - } | ||
| 258 | - | ||
| 259 | - $el.off('tap') | ||
| 260 | - $el.on('tap', async () => { | ||
| 261 | - const fileName = | ||
| 262 | - $el.find('span span span').first().text() || | ||
| 263 | - $el.text().trim().substring(0, 50) || | ||
| 264 | - getFileNameFromUrl(dataHref) | ||
| 265 | - | ||
| 266 | - await openFileLink(dataHref, fileName) | ||
| 267 | - }) | ||
| 268 | - }) | ||
| 269 | - }) | ||
| 270 | } | 432 | } |
| 271 | 433 | ||
| 272 | -const bindLinkLongPressEvents = () => { | 434 | +const handleRichTextLongTap = event => { |
| 273 | - nextTick(() => { | 435 | + const url = getNodeUrlFromEvent(event) |
| 274 | - const container = $(containerSelector) | ||
| 275 | - const richTextLinks = container.find('.rich-text-link') | ||
| 276 | - const anchorLinks = container.find('a[href]') | ||
| 277 | - const fileLinks = container.find('._file_list') | ||
| 278 | - | ||
| 279 | - let allLinks = [] | ||
| 280 | - if (richTextLinks.length > 0) { | ||
| 281 | - allLinks = allLinks.concat(richTextLinks.toArray()) | ||
| 282 | - } | ||
| 283 | - if (anchorLinks.length > 0) { | ||
| 284 | - allLinks = allLinks.concat(anchorLinks.toArray()) | ||
| 285 | - } | ||
| 286 | - if (fileLinks.length > 0) { | ||
| 287 | - allLinks = allLinks.concat(fileLinks.toArray()) | ||
| 288 | - } | ||
| 289 | - | ||
| 290 | - allLinks.forEach(el => { | ||
| 291 | - const $el = $(el) | ||
| 292 | - const dataHref = $el.attr('data-href') || $el.attr('href') | ||
| 293 | 436 | ||
| 294 | - if (!dataHref) { | 437 | + if (!url) { |
| 295 | return | 438 | return |
| 296 | } | 439 | } |
| 297 | 440 | ||
| 298 | - $el.off('longpress') | 441 | + copyLink(url, getFileNameFromUrl(url)) |
| 299 | - $el.on('longpress', () => { | ||
| 300 | - const fileName = | ||
| 301 | - $el.find('span span span').first().text() || | ||
| 302 | - $el.text().trim().substring(0, 30) || | ||
| 303 | - getFileNameFromUrl(dataHref) | ||
| 304 | - | ||
| 305 | - copyLink(dataHref, fileName) | ||
| 306 | - }) | ||
| 307 | - }) | ||
| 308 | - }) | ||
| 309 | } | 442 | } |
| 310 | - | ||
| 311 | -const handleContentChange = () => { | ||
| 312 | - processContent(props.content) | ||
| 313 | - | ||
| 314 | - nextTick(() => { | ||
| 315 | - // 富文本每次重渲染都会替换节点,需要重新挂载图片预览和链接交互事件。 | ||
| 316 | - bindImageEvents() | ||
| 317 | - bindFileLinkEvents() | ||
| 318 | - bindLinkLongPressEvents() | ||
| 319 | - }) | ||
| 320 | -} | ||
| 321 | - | ||
| 322 | -watch(() => props.content, handleContentChange, { immediate: true }) | ||
| 323 | -watch(() => props.enableTransform, setupTransformElement, { immediate: true }) | ||
| 324 | - | ||
| 325 | -onBeforeUnmount(() => { | ||
| 326 | - Taro.options.html.transformElement = previousTransformElement | ||
| 327 | -}) | ||
| 328 | </script> | 443 | </script> |
| 329 | 444 | ||
| 330 | <style lang="less"> | 445 | <style lang="less"> |
| 331 | -#rich-text-renderer, | ||
| 332 | .rich-text-renderer { | 446 | .rich-text-renderer { |
| 333 | - color: #4b5563; | ||
| 334 | - font-size: 30rpx; | ||
| 335 | - line-height: 1.8; | ||
| 336 | - word-break: break-word; | ||
| 337 | - | ||
| 338 | - .h5-html, | ||
| 339 | - .h5-address, | ||
| 340 | - .h5-blockquote, | ||
| 341 | - .h5-body, | ||
| 342 | - .h5-dd, | ||
| 343 | - .h5-div, | ||
| 344 | - .h5-dl, | ||
| 345 | - .h5-dt, | ||
| 346 | - .h5-fieldset, | ||
| 347 | - .h5-form, | ||
| 348 | - .h5-frame, | ||
| 349 | - .h5-frameset, | ||
| 350 | - .h5-h1, | ||
| 351 | - .h5-h2, | ||
| 352 | - .h5-h3, | ||
| 353 | - .h5-h4, | ||
| 354 | - .h5-h5, | ||
| 355 | - .h5-h6, | ||
| 356 | - .h5-noframes, | ||
| 357 | - .h5-ol, | ||
| 358 | - .h5-p, | ||
| 359 | - .h5-ul, | ||
| 360 | - .h5-center, | ||
| 361 | - .h5-dir, | ||
| 362 | - .h5-hr, | ||
| 363 | - .h5-menu, | ||
| 364 | - .h5-pre { | ||
| 365 | display: block; | 447 | display: block; |
| 366 | - unicode-bidi: embed; | ||
| 367 | - } | ||
| 368 | - | ||
| 369 | - .h5-li { | ||
| 370 | - display: list-item; | ||
| 371 | - margin-bottom: 12rpx; | ||
| 372 | - } | ||
| 373 | - | ||
| 374 | - .h5-head { | ||
| 375 | - display: none; | ||
| 376 | - } | ||
| 377 | - | ||
| 378 | - .h5-p, | ||
| 379 | - .h5-div, | ||
| 380 | - .h5-blockquote, | ||
| 381 | - .h5-ul, | ||
| 382 | - .h5-ol { | ||
| 383 | - margin: 0 0 20rpx; | ||
| 384 | - } | ||
| 385 | - | ||
| 386 | - .h5-ul, | ||
| 387 | - .h5-ol { | ||
| 388 | - padding-left: 36rpx; | ||
| 389 | - } | ||
| 390 | - | ||
| 391 | - .h5-img, | ||
| 392 | - img { | ||
| 393 | - width: 100%; | ||
| 394 | - max-width: 100%; | ||
| 395 | - height: auto; | ||
| 396 | - display: block; | ||
| 397 | - margin: 24rpx 0; | ||
| 398 | - border-radius: 16rpx; | ||
| 399 | - } | ||
| 400 | - | ||
| 401 | - .rich-text-link, | ||
| 402 | - .h5-a, | ||
| 403 | - a, | ||
| 404 | - ._file_list { | ||
| 405 | - color: #2563eb; | ||
| 406 | - text-decoration: underline; | ||
| 407 | - word-break: break-all; | ||
| 408 | - } | ||
| 409 | - | ||
| 410 | - .rich-text-link *, | ||
| 411 | - ._file_list * { | ||
| 412 | - pointer-events: none; | ||
| 413 | - } | ||
| 414 | - | ||
| 415 | - .h5-b, | ||
| 416 | - .h5-strong { | ||
| 417 | - font-weight: bolder; | ||
| 418 | - } | ||
| 419 | - | ||
| 420 | - .h5-i, | ||
| 421 | - .h5-em { | ||
| 422 | - font-style: italic; | ||
| 423 | - } | ||
| 424 | - | ||
| 425 | - .h5-table { | ||
| 426 | - display: table; | ||
| 427 | - width: 100%; | ||
| 428 | - border-spacing: 2px; | ||
| 429 | - margin: 20rpx 0; | ||
| 430 | - } | ||
| 431 | - | ||
| 432 | - .h5-tr { | ||
| 433 | - display: table-row; | ||
| 434 | - } | ||
| 435 | - | ||
| 436 | - .h5-td, | ||
| 437 | - .h5-th { | ||
| 438 | - display: table-cell; | ||
| 439 | - padding: 12rpx; | ||
| 440 | - border: 1rpx solid #d1d5db; | ||
| 441 | - vertical-align: top; | ||
| 442 | - } | ||
| 443 | - | ||
| 444 | - .h5-th { | ||
| 445 | - font-weight: bolder; | ||
| 446 | - background: #f9fafb; | ||
| 447 | - } | ||
| 448 | } | 448 | } |
| 449 | </style> | 449 | </style> | ... | ... |
-
Please register or login to post a comment