hookehuyr

feat(rich-text): 新增 RichTextRenderer 富文本渲染组件

新增功能:
- HTML 实体自动解码( , &, <, >, ", ')
- <a> 标签自动替换为 <div data-href="">
- 图片长按预览功能
- PDF 文件链接点击处理
- transformElement 图片自动处理(默认启用)
- 支持图片 mode="widthFix" 和 style="width: 100%"

组件 API:
- content: HTML 内容(必需)
- enable-transform: 是否启用图片处理(默认 true)
- @image-preview: 图片预览事件
- @file-click: 文件点击事件

测试页面:
- 简化为使用 RichTextRenderer 组件
- 保留 realApiData 测试数据

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 +<!--
2 + * @Date: 2026-02-27
3 + * @Description: 富文本渲染组件 - 基于 Taro v-html
4 + *
5 + * @features
6 + * - HTML 实体自动解码
7 + * - <a> 标签自动替换为 <div data-href="">
8 + * - 图片长按预览
9 + * - 文件链接点击处理
10 + * - 可选的 transformElement 图片自动处理
11 + *
12 + * @example
13 + * <RichTextRenderer
14 + * :content="htmlContent"
15 + * :enable-transform="true"
16 + * @image-preview="handlePreview"
17 + * @file-click="handleFileClick"
18 + * />
19 +-->
20 +<template>
21 + <view id="rich-text-renderer" v-html="processedContent" class="rich-text-content"></view>
22 +</template>
23 +
24 +<script setup>
25 +import { ref, watch, onMounted, nextTick } from 'vue'
26 +import Taro from '@tarojs/taro'
27 +import { $ } from '@tarojs/extend'
28 +import { useFileOperation } from '@/composables/useFileOperation'
29 +import '@tarojs/taro/html.css'
30 +
31 +/**
32 + * 组件属性
33 + */
34 +const props = defineProps({
35 + /**
36 + * HTML 内容字符串
37 + */
38 + content: {
39 + type: String,
40 + default: ''
41 + },
42 + /**
43 + * 是否启用 transformElement 自动处理图片
44 + * @description 开启后会为非链接内的图片添加 mode="widthFix" 和 style="width: 100%"
45 + * @default true
46 + */
47 + enableTransform: {
48 + type: Boolean,
49 + default: true
50 + }
51 +})
52 +
53 +/**
54 + * 组件事件
55 + */
56 +const emit = defineEmits(['image-preview', 'file-click'])
57 +
58 +// 文件操作
59 +const { viewFile } = useFileOperation()
60 +
61 +// 处理后的内容
62 +const processedContent = ref('')
63 +
64 +// 容器 ID(用于 jQuery 选择器)
65 +const CONTAINER_ID = '#rich-text-renderer'
66 +
67 +/**
68 + * HTML 实体解码
69 + */
70 +const decodeHtmlEntities = (html) => {
71 + return html
72 + .replace(/&nbsp;/g, ' ') // 空格
73 + .replace(/&amp;/g, '&') // &
74 + .replace(/&lt;/g, '<') // <
75 + .replace(/&gt;/g, '>') // >
76 + .replace(/&quot;/g, '"') // "
77 + .replace(/&apos;/g, "'") // '
78 +}
79 +
80 +/**
81 + * 替换 <a> 标签为 <div data-href="">
82 + */
83 +const replaceAnchorTags = (html) => {
84 + let content = html
85 + // 替换 <a ... href="..."> 为 <div ... data-href="..." data-is-link="true">
86 + content = content.replace(/<a\s+/g, '<div data-is-link="true" ')
87 + content = content.replace(/href=/g, 'data-href=')
88 + content = content.replace(/<\/a>/g, '</div>')
89 + return content
90 +}
91 +
92 +/**
93 + * 处理 HTML 内容
94 + */
95 +const processContent = (raw) => {
96 + if (!raw) {
97 + processedContent.value = ''
98 + return
99 + }
100 +
101 + let processed = raw
102 +
103 + // 1. HTML 实体解码
104 + processed = decodeHtmlEntities(processed)
105 +
106 + // 2. 替换 <a> 标签
107 + processed = replaceAnchorTags(processed)
108 +
109 + processedContent.value = processed
110 +}
111 +
112 +/**
113 + * 配置 transformElement
114 + */
115 +const setupTransformElement = () => {
116 + if (props.enableTransform) {
117 + Taro.options.html.transformElement = (element) => {
118 + const el = element
119 + // 获取标签名(兼容大小写)
120 + const nodeName = el.nodeName?.toLowerCase() || ''
121 + const tagName = el.tagName?.toLowerCase() || ''
122 +
123 + // 只处理 img/image 标签
124 + const isImg = nodeName === 'img' || tagName === 'img' || nodeName === 'image' || tagName === 'image'
125 + if (!isImg) {
126 + return el
127 + }
128 +
129 + // 检查是否在链接内(遍历整个父元素链)
130 + let parent = el.parentElement
131 + let isInsideLink = false
132 +
133 + while (parent) {
134 + const pName = parent.nodeName?.toLowerCase() || ''
135 + const pDataIsLink = parent.getAttribute?.('data-is-link')
136 + const pClassList = parent.classList?.value || ''
137 +
138 + // 检查是否在 <a> 或带有 data-is-link 或 _file_list class 的元素内
139 + if (pName === 'a' || pDataIsLink === 'true' || pClassList.includes('_file_list')) {
140 + isInsideLink = true
141 + break
142 + }
143 +
144 + parent = parent.parentElement
145 + }
146 +
147 + if (isInsideLink) {
148 + // 在链接内的 img(图标),不处理但必须返回元素
149 + return el
150 + }
151 +
152 + // 在链接外的 img(内容图片),添加 mode="widthFix" 和 style
153 + if (el.setAttribute) {
154 + el.setAttribute('mode', 'widthFix')
155 + el.setAttribute('style', 'width: 100%;')
156 + }
157 +
158 + return el
159 + }
160 + } else {
161 + Taro.options.html.transformElement = undefined
162 + }
163 +}
164 +
165 +/**
166 + * 绑定图片长按预览事件
167 + */
168 +const bindImageEvents = () => {
169 + nextTick(() => {
170 + const imgs = $(CONTAINER_ID).children('.h5-p').children('.h5-img')
171 +
172 + imgs.forEach(function (img) {
173 + $(img).off('longpress') // 先解绑,避免重复
174 + $(img).on('longpress', function () {
175 + const src = $(img).attr('src')
176 +
177 + Taro.previewImage({
178 + urls: [src],
179 + current: src,
180 + indicator: 'default',
181 + loop: false,
182 + success: () => {
183 + emit('image-preview', { src })
184 + },
185 + fail: () => {
186 + // 预览失败
187 + }
188 + })
189 + })
190 + })
191 + })
192 +}
193 +
194 +/**
195 + * 绑定文件链接点击事件
196 + */
197 +const bindFileLinkEvents = () => {
198 + nextTick(() => {
199 + const container = $(CONTAINER_ID)
200 + const fileLinks = container.find('div[data-is-link="true"]')
201 +
202 + // 如果找不到 data-is-link,尝试用 class 查找
203 + const targetLinks = fileLinks.length > 0 ? fileLinks : container.find('._file_list')
204 +
205 + targetLinks.each(function (idx, el) {
206 + const $el = $(el)
207 + const dataHref = $el.attr('data-href')
208 +
209 + if (dataHref) {
210 + $el.off('tap') // 先解绑,避免重复
211 + $el.on('tap', function () {
212 + // 尝试提取文件名
213 + const fileName = $el.find('span span span').first().text() ||
214 + $el.text().trim().substring(0, 50) ||
215 + 'document.pdf'
216 +
217 + emit('file-click', { url: dataHref, fileName })
218 + viewFile({ downloadUrl: dataHref, fileName })
219 + })
220 + }
221 + })
222 + })
223 +}
224 +
225 +/**
226 + * 处理内容变化
227 + */
228 +const handleContentChange = () => {
229 + processContent(props.content)
230 +
231 + // 等待 DOM 更新后绑定事件
232 + nextTick(() => {
233 + bindImageEvents()
234 + bindFileLinkEvents()
235 + })
236 +}
237 +
238 +/**
239 + * 监听 props 变化
240 + */
241 +watch(() => props.content, handleContentChange, { immediate: true })
242 +watch(() => props.enableTransform, () => {
243 + setupTransformElement()
244 + // 重新渲染当前内容
245 + if (processedContent.value) {
246 + const current = processedContent.value
247 + processedContent.value = ''
248 + nextTick(() => {
249 + processedContent.value = current
250 + handleContentChange()
251 + })
252 + }
253 +})
254 +
255 +/**
256 + * 组件挂载
257 + */
258 +onMounted(() => {
259 + setupTransformElement()
260 + handleContentChange()
261 +})
262 +</script>
263 +
264 +<style lang="less" scoped>
265 +.rich-text-content {
266 + // 继承 Taro HTML 样式
267 + :deep(img) {
268 + max-width: 100%;
269 + height: auto;
270 + }
271 +
272 + // 文件链接样式
273 + :deep(div[data-is-link="true"]) {
274 + cursor: pointer;
275 + transition: opacity 0.2s;
276 +
277 + &:active {
278 + opacity: 0.7;
279 + }
280 + }
281 +}
282 +</style>
1 <!-- 1 <!--
2 * @Date: 2026-02-27 2 * @Date: 2026-02-27
3 - * @Description: v-html 测试页面 - 验证 v-html 在 Taro 中是否可用 3 + * @Description: v-html 测试页面 - 使用 RichTextRenderer 组件
4 * 4 *
5 * @purpose 5 * @purpose
6 - * 验证 v-html 指令在 Taro + Vue3 小程序中是否能正常运行 6 + * 测试 RichTextRenderer 组件的富文本渲染功能
7 * 对比 index1.vue 中 mp-html 组件的绑定方式 7 * 对比 index1.vue 中 mp-html 组件的绑定方式
8 * 8 *
9 * @test-cases 9 * @test-cases
10 - * 1. v-html 渲染响应式变量 10 + * 1. 组件渲染响应式变量
11 * 2. 内容切换响应性 11 * 2. 内容切换响应性
12 - * 3. HTML 标签解析 12 + * 3. HTML 实体解析
13 + * 4. 图片长按预览
14 + * 5. 文件链接点击
13 --> 15 -->
14 <template> 16 <template>
15 <view class="rich-text-test-page"> 17 <view class="rich-text-test-page">
16 <!-- 导航栏 --> 18 <!-- 导航栏 -->
17 - <NavHeader title="v-html 测试" /> 19 + <NavHeader title="RichTextRenderer 测试" />
18 20
19 <view class="test-container"> 21 <view class="test-container">
20 <!-- 测试说明 --> 22 <!-- 测试说明 -->
21 <view class="test-info"> 23 <view class="test-info">
22 <text class="test-title">测试项目:</text> 24 <text class="test-title">测试项目:</text>
23 - <text class="test-item">1. v-html 渲染 {{ renderTest ? '✅' : '❌' }}</text> 25 + <text class="test-item">1. 组件渲染 {{ renderTest ? '✅' : '❌' }}</text>
24 <text class="test-item">2. 内容切换 {{ contentSwitchTest ? '✅' : '❌' }}</text> 26 <text class="test-item">2. 内容切换 {{ contentSwitchTest ? '✅' : '❌' }}</text>
25 - <text class="test-item">3. HTML 解析 {{ htmlParseTest ? '✅' : '❌' }}</text> 27 + <text class="test-item">3. 图片预览 {{ imagePreviewTest ? '✅' : '❌' }}</text>
28 + <text class="test-item">4. 文件链接 {{ fileLinkTest ? '✅' : '❌' }}</text>
26 </view> 29 </view>
27 30
28 - <!-- v-html 渲染区域 --> 31 + <!-- RichTextRenderer 渲染区域 -->
29 - <view class="v-html-container"> 32 + <view class="renderer-container">
30 - <text class="container-title">v-html 绑定区域:</text> 33 + <text class="container-title">RichTextRenderer 渲染区域:</text>
31 - <view id="taro_html" v-html="testHtml" class="taro_html"></view> 34 + <view class="renderer-wrapper">
35 + <RichTextRenderer
36 + :content="testHtml"
37 + :enable-transform="transformEnabled"
38 + @image-preview="handleImagePreview"
39 + @file-click="handleFileClick"
40 + />
41 + </view>
32 </view> 42 </view>
33 43
34 <!-- 测试结果 --> 44 <!-- 测试结果 -->
...@@ -45,11 +55,9 @@ ...@@ -45,11 +55,9 @@
45 <view class="action-btn primary" @tap="switchTestContent(1)">测试1: 纯文本</view> 55 <view class="action-btn primary" @tap="switchTestContent(1)">测试1: 纯文本</view>
46 <view class="action-btn primary" @tap="switchTestContent(2)">测试2: HTML标签</view> 56 <view class="action-btn primary" @tap="switchTestContent(2)">测试2: HTML标签</view>
47 <view class="action-btn primary" @tap="switchTestContent(3)">测试3: 图片</view> 57 <view class="action-btn primary" @tap="switchTestContent(3)">测试3: 图片</view>
48 - <view class="action-btn primary" @tap="switchTestContent(4)">测试4: 链接</view>
49 <view class="action-btn warning" @tap="clearContent">清空内容</view> 58 <view class="action-btn warning" @tap="clearContent">清空内容</view>
50 <view class="action-btn success" @tap="loadFromAPI">模拟API加载</view> 59 <view class="action-btn success" @tap="loadFromAPI">模拟API加载</view>
51 <view class="action-btn danger" @tap="loadRealApiData">📄 真实API</view> 60 <view class="action-btn danger" @tap="loadRealApiData">📄 真实API</view>
52 - <view class="action-btn info" @tap="loadRealApiDataWithDiv">📄 真实API(a→div)</view>
53 <view class="action-btn" :class="transformEnabled ? 'enabled' : ''" @tap="toggleTransform"> 61 <view class="action-btn" :class="transformEnabled ? 'enabled' : ''" @tap="toggleTransform">
54 {{ transformEnabled ? '✅' : '⚪' }} 图片自动处理 62 {{ transformEnabled ? '✅' : '⚪' }} 图片自动处理
55 </view> 63 </view>
...@@ -57,41 +65,37 @@ ...@@ -57,41 +65,37 @@
57 65
58 <!-- 对比说明 --> 66 <!-- 对比说明 -->
59 <view class="compare-note"> 67 <view class="compare-note">
60 - <text class="note-title">与 index1.vue 对比:</text> 68 + <text class="note-title">组件说明:</text>
61 - <text class="note-item">• index1.vue 使用 &lt;mp-html&gt; 组件 + :content="testHtml"</text> 69 + <text class="note-item">• RichTextRenderer 基于 Taro v-html</text>
62 - <text class="note-item">• 本页使用 v-html="testHtml" 指令</text> 70 + <text class="note-item">• 自动解析 HTML 实体(&nbsp; &amp; 等)</text>
63 - <text class="note-item">• 两者都绑定响应式变量 testHtml</text> 71 + <text class="note-item">• 自动替换 &lt;a&gt; 为 &lt;div data-href=""&gt;</text>
72 + <text class="note-item">• 支持图片长按预览和文件链接点击</text>
64 </view> 73 </view>
65 </view> 74 </view>
66 </view> 75 </view>
67 </template> 76 </template>
68 77
69 <script setup> 78 <script setup>
70 -import { ref, onMounted, nextTick, watch } from 'vue' 79 +import { ref, onMounted } from 'vue'
71 import Taro, { definePageConfig } from '@tarojs/taro' 80 import Taro, { definePageConfig } from '@tarojs/taro'
72 import NavHeader from '@/components/navigation/NavHeader.vue' 81 import NavHeader from '@/components/navigation/NavHeader.vue'
73 -import { $ } from '@tarojs/extend' 82 +import RichTextRenderer from '@/components/RichTextRenderer.vue'
74 -import { useFileOperation } from '@/composables/useFileOperation'
75 -// v-html 需要导入 Taro 的 HTML 样式
76 -import '@tarojs/taro/html.css'
77 -
78 -// 文件操作
79 -const { viewFile } = useFileOperation()
80 83
81 // 配置页面 84 // 配置页面
82 definePageConfig({ 85 definePageConfig({
83 - navigationBarTitleText: 'v-html 测试' 86 + navigationBarTitleText: 'RichTextRenderer 测试'
84 }) 87 })
85 88
86 // 测试状态 89 // 测试状态
87 const renderTest = ref(false) 90 const renderTest = ref(false)
88 const contentSwitchTest = ref(false) 91 const contentSwitchTest = ref(false)
89 -const htmlParseTest = ref(false) 92 +const imagePreviewTest = ref(false)
93 +const fileLinkTest = ref(false)
90 const switchCount = ref(0) 94 const switchCount = ref(0)
91 const currentContentType = ref('未加载') 95 const currentContentType = ref('未加载')
92 const contentLength = ref(0) 96 const contentLength = ref(0)
93 const lastAction = ref('页面初始化') 97 const lastAction = ref('页面初始化')
94 -const transformEnabled = ref(false) // 是否启用 transformElement 98 +const transformEnabled = ref(true)
95 99
96 // 测试 HTML 内容 100 // 测试 HTML 内容
97 const testHtml = ref('') 101 const testHtml = ref('')
...@@ -187,7 +191,7 @@ const testContents = { ...@@ -187,7 +191,7 @@ const testContents = {
187 name: '纯文本', 191 name: '纯文本',
188 content: ` 192 content: `
189 <h1>测试1:纯文本渲染</h1> 193 <h1>测试1:纯文本渲染</h1>
190 - <p>这是一段纯文本内容,测试 v-html 是否能正确渲染基本的 HTML 结构。</p> 194 + <p>这是一段纯文本内容,测试 RichTextRenderer 是否能正确渲染基本的 HTML 结构。</p>
191 <p><strong>加粗文本</strong>和<em>斜体文本</em>测试。</p> 195 <p><strong>加粗文本</strong>和<em>斜体文本</em>测试。</p>
192 ` 196 `
193 }, 197 },
...@@ -214,23 +218,21 @@ const testContents = { ...@@ -214,23 +218,21 @@ const testContents = {
214 <img class="h5-img" src="https://cdn.ipadbiz.cn/space_3079606/微信图片_20260227170749_FhZ6SHYxM9yVKOPo1LeqVO25I35q.jpg" alt="测试图片" style="max-width: 100%;" /> 218 <img class="h5-img" src="https://cdn.ipadbiz.cn/space_3079606/微信图片_20260227170749_FhZ6SHYxM9yVKOPo1LeqVO25I35q.jpg" alt="测试图片" style="max-width: 100%;" />
215 </p> 219 </p>
216 ` 220 `
217 - },
218 - 4: {
219 - name: '链接',
220 - content: `
221 - <h1>测试4:链接渲染</h1>
222 - <p>测试链接是否正确渲染:</p>
223 - <p>
224 - <a href="https://www.baidu.com">百度链接</a> |
225 - <a href="https://www.github.com">GitHub 链接</a>
226 - </p>
227 - <p>PDF 链接测试:
228 - <a href="https://cdn.ipadbiz.cn/space_3079606/需提供的部分个人资料SAMPLE(不含流水SAMPLE)_扫描版_lkAqOTlyK-jZ7RbBN43oiG5QPfr-.pdf">PDF 文档</a>
229 - </p>
230 - `
231 } 221 }
232 } 222 }
233 223
224 +// 处理图片预览事件
225 +const handleImagePreview = ({ src }) => {
226 + imagePreviewTest.value = true
227 + lastAction.value = `图片预览: ${src.substring(0, 50)}...`
228 +}
229 +
230 +// 处理文件点击事件
231 +const handleFileClick = ({ fileName }) => {
232 + fileLinkTest.value = true
233 + lastAction.value = `文件点击: ${fileName}`
234 +}
235 +
234 // 切换测试内容 236 // 切换测试内容
235 const switchTestContent = (index) => { 237 const switchTestContent = (index) => {
236 const template = testContents[index] 238 const template = testContents[index]
...@@ -241,11 +243,7 @@ const switchTestContent = (index) => { ...@@ -241,11 +243,7 @@ const switchTestContent = (index) => {
241 switchCount.value++ 243 switchCount.value++
242 lastAction.value = `切换到测试${index}: ${template.name}` 244 lastAction.value = `切换到测试${index}: ${template.name}`
243 contentSwitchTest.value = true 245 contentSwitchTest.value = true
244 - 246 + renderTest.value = true
245 - // 绑定图片长按事件
246 - nextTick(() => {
247 - bindImageEvents()
248 - })
249 } 247 }
250 } 248 }
251 249
...@@ -263,7 +261,6 @@ const loadFromAPI = () => { ...@@ -263,7 +261,6 @@ const loadFromAPI = () => {
263 Taro.showLoading({ title: '加载中...' }) 261 Taro.showLoading({ title: '加载中...' })
264 262
265 setTimeout(() => { 263 setTimeout(() => {
266 - // 模拟 API 返回的数据
267 const apiData = ` 264 const apiData = `
268 <div class="api-content"> 265 <div class="api-content">
269 <h1>从 API 加载的内容</h1> 266 <h1>从 API 加载的内容</h1>
...@@ -278,34 +275,28 @@ const loadFromAPI = () => { ...@@ -278,34 +275,28 @@ const loadFromAPI = () => {
278 currentContentType.value = 'API 数据' 275 currentContentType.value = 'API 数据'
279 contentLength.value = apiData.length 276 contentLength.value = apiData.length
280 lastAction.value = 'API 加载完成' 277 lastAction.value = 'API 加载完成'
278 + renderTest.value = true
281 Taro.hideLoading() 279 Taro.hideLoading()
282 -
283 - nextTick(() => {
284 - bindImageEvents()
285 - })
286 }, 1000) 280 }, 1000)
287 } 281 }
288 282
289 -// 加载真实 API 数据(原始版本,包含 <a> 标签) 283 +// 加载真实 API 数据
290 const loadRealApiData = () => { 284 const loadRealApiData = () => {
291 - lastAction.value = '加载真实 API 数据(原始)...' 285 + lastAction.value = '加载真实 API 数据...'
292 Taro.showLoading({ title: '加载中...' }) 286 Taro.showLoading({ title: '加载中...' })
293 287
294 setTimeout(() => { 288 setTimeout(() => {
295 testHtml.value = realApiData.post_content 289 testHtml.value = realApiData.post_content
296 - currentContentType.value = `真实文章(原始<a>): ${realApiData.post_title}` 290 + currentContentType.value = `真实文章: ${realApiData.post_title}`
297 contentLength.value = realApiData.post_content.length 291 contentLength.value = realApiData.post_content.length
298 switchCount.value++ 292 switchCount.value++
299 - lastAction.value = `加载文章 ID: ${realApiData.id} (原始<a>标签)` 293 + lastAction.value = `加载文章 ID: ${realApiData.id}`
294 + renderTest.value = true
300 Taro.hideLoading() 295 Taro.hideLoading()
301 296
302 - nextTick(() => {
303 - bindImageEvents()
304 - })
305 -
306 Taro.showToast({ 297 Taro.showToast({
307 - title: '原始 <a> 标签', 298 + title: '组件自动处理',
308 - icon: 'none' 299 + icon: 'success'
309 }) 300 })
310 }, 500) 301 }, 500)
311 } 302 }
...@@ -313,220 +304,15 @@ const loadRealApiData = () => { ...@@ -313,220 +304,15 @@ const loadRealApiData = () => {
313 // 切换 transformElement 304 // 切换 transformElement
314 const toggleTransform = () => { 305 const toggleTransform = () => {
315 transformEnabled.value = !transformEnabled.value 306 transformEnabled.value = !transformEnabled.value
316 - 307 + lastAction.value = transformEnabled.value ? '已启用图片自动处理' : '已禁用图片自动处理'
317 - if (transformEnabled.value) { 308 + Taro.showToast({
318 - // 启用 transformElement 309 + title: transformEnabled.value ? '已启用' : '已禁用',
319 - Taro.options.html.transformElement = (el) => { 310 + icon: transformEnabled.value ? 'success' : 'none'
320 - const nodeName = el.nodeName?.toLowerCase() || ''
321 - const tagName = el.tagName?.toLowerCase() || ''
322 -
323 - // 处理 img/image 标签
324 - if (nodeName === 'img' || tagName === 'img' || nodeName === 'image' || tagName === 'image') {
325 - const src = el.getAttribute('src') || ''
326 -
327 - // 检查父元素链
328 - let parent = el.parentElement
329 - let isInsideLink = false
330 - let parentChain = []
331 -
332 - while (parent) {
333 - const pName = parent.nodeName?.toLowerCase() || ''
334 - const pDataIsLink = parent.getAttribute?.('data-is-link')
335 - const pClassList = parent.classList?.value || ''
336 -
337 - parentChain.push(pName)
338 -
339 - if (pName === 'a' || pDataIsLink === 'true' || pClassList.includes('_file_list')) {
340 - isInsideLink = true
341 - break
342 - }
343 -
344 - parent = parent.parentElement
345 - }
346 -
347 - if (isInsideLink) {
348 - // 在链接内,不处理(图标),但必须返回元素让它显示
349 - return el // 直接返回,不做任何修改
350 - }
351 -
352 - // 不在链接内,处理(内容图片)
353 - el.setAttribute('mode', 'widthFix')
354 - el.setAttribute('style', 'width: 100%;')
355 - }
356 -
357 - // 所有元素都必须返回,否则会被过滤掉不显示
358 - return el
359 - }
360 -
361 - lastAction.value = '已启用 transformElement(外部图片自动 widthFix)'
362 - Taro.showToast({
363 - title: '已启用图片自动处理',
364 - icon: 'success'
365 - })
366 -
367 - // 重新加载当前内容以应用 transform
368 - if (testHtml.value) {
369 - const currentContent = testHtml.value
370 - testHtml.value = ''
371 - nextTick(() => {
372 - testHtml.value = currentContent
373 - })
374 - }
375 - } else {
376 - // 禁用 transformElement
377 - Taro.options.html.transformElement = undefined
378 -
379 - lastAction.value = '已禁用 transformElement'
380 - Taro.showToast({
381 - title: '已禁用图片自动处理',
382 - icon: 'none'
383 - })
384 -
385 - // 重新加载当前内容
386 - if (testHtml.value) {
387 - const currentContent = testHtml.value
388 - testHtml.value = ''
389 - nextTick(() => {
390 - testHtml.value = currentContent
391 - })
392 - }
393 - }
394 -}
395 -
396 -// 加载真实 API 数据(<a> 替换为 <div> 版本)
397 -const loadRealApiDataWithDiv = () => {
398 - lastAction.value = '加载真实 API 数据(a→div)...'
399 - Taro.showLoading({ title: '加载中...' })
400 -
401 - setTimeout(() => {
402 - // 将 <a> 标签替换为 <div> 标签,保留 href 为 data-href
403 - let content = realApiData.post_content
404 -
405 - // 替换 HTML 实体
406 - content = content.replace(/&nbsp;/g, ' ') // 空格
407 - content = content.replace(/&amp;/g, '&') // &
408 - content = content.replace(/&lt;/g, '<') // <
409 - content = content.replace(/&gt;/g, '>') // >
410 - content = content.replace(/&quot;/g, '"') // "
411 - content = content.replace(/&apos;/g, "'") // '
412 -
413 - // 替换 <a ... href="..."> 为 <div ... data-href="..." data-is-link="true">
414 - content = content.replace(/<a\s+/g, '<div data-is-link="true" ')
415 - content = content.replace(/href=/g, 'data-href=')
416 - content = content.replace(/<\/a>/g, '</div>')
417 -
418 - // 统计 data-href 数量
419 - const hrefCount = (content.match(/data-href=/g) || []).length
420 -
421 - testHtml.value = content
422 - currentContentType.value = `真实文章(<a>→<div>): ${realApiData.post_title}`
423 - contentLength.value = content.length
424 - switchCount.value++
425 - lastAction.value = `加载文章 ID: ${realApiData.id} (a标签已替换为div, href=${hrefCount})`
426 - Taro.hideLoading()
427 -
428 - nextTick(() => {
429 - bindImageEvents()
430 - // 绑定文件链接点击事件
431 - bindFileLinkEvents()
432 - })
433 -
434 - Taro.showToast({
435 - title: `已绑定 ${hrefCount} 个文件链接`,
436 - icon: 'success'
437 - })
438 - }, 500)
439 -}
440 -
441 -// 绑定图片长按预览事件(类似 activityDetail 页面的实现)
442 -const bindImageEvents = () => {
443 - const imgs = $('#taro_html').children('.h5-p').children('.h5-img')
444 -
445 - imgs.forEach(function (img) {
446 - $(img).on('longpress', function () {
447 - const src = $(img).attr('src')
448 -
449 - Taro.previewImage({
450 - urls: [src],
451 - current: src,
452 - indicator: 'default',
453 - loop: false,
454 - success: res => {
455 - lastAction.value = '图片预览触发'
456 - },
457 - fail: err => {
458 - // 预览失败
459 - }
460 - })
461 - })
462 - })
463 -
464 - htmlParseTest.value = imgs.length > 0
465 -}
466 -
467 -// 绑定文件链接点击事件
468 -const bindFileLinkEvents = () => {
469 - // 先检查 #taro_html 容器是否存在
470 - const container = $('#taro_html')
471 -
472 - // 检查带 data-is-link 属性的 div
473 - const fileLinks = container.find('div[data-is-link="true"]')
474 -
475 - // 如果找不到,尝试用 class 查找
476 - if (fileLinks.length === 0) {
477 - const classLinks = container.find('._file_list')
478 -
479 - // 如果用 class 找到了,绑定到这些元素上
480 - classLinks.each(function (idx, el) {
481 - const $el = $(el)
482 - const dataHref = $el.attr('data-href')
483 -
484 - if (dataHref) {
485 - $el.on('tap', function () {
486 - const fileName = $el.find('span span span').first().text() || 'document.pdf'
487 - lastAction.value = `打开文件: ${fileName}`
488 - viewFile({
489 - downloadUrl: dataHref,
490 - fileName: fileName
491 - })
492 - })
493 - }
494 - })
495 -
496 - return classLinks.length
497 - }
498 -
499 - // 原始逻辑:使用 data-is-link 查找
500 - fileLinks.forEach(function (link) {
501 - const $link = $(link)
502 - const href = $link.attr('data-href')
503 -
504 - $link.on('tap', function () {
505 - const fileName = $link.find('span span span').first().text() || 'document.pdf'
506 -
507 - if (href) {
508 - lastAction.value = `打开文件: ${fileName}`
509 -
510 - // 使用 useFileOperation 打开文件
511 - viewFile({
512 - downloadUrl: href,
513 - fileName: fileName
514 - })
515 - }
516 - })
517 }) 311 })
518 -
519 - return fileLinks.length
520 } 312 }
521 313
522 -// 监听 testHtml 变化
523 -watch(testHtml, (_newVal, _oldVal) => {
524 - renderTest.value = true
525 -})
526 -
527 // 初始化 314 // 初始化
528 onMounted(() => { 315 onMounted(() => {
529 - // 加载默认测试内容
530 switchTestContent(1) 316 switchTestContent(1)
531 }) 317 })
532 </script> 318 </script>
...@@ -562,7 +348,7 @@ onMounted(() => { ...@@ -562,7 +348,7 @@ onMounted(() => {
562 margin: 8rpx 0; 348 margin: 8rpx 0;
563 } 349 }
564 350
565 -.v-html-container { 351 +.renderer-container {
566 background: #fff; 352 background: #fff;
567 padding: 24rpx; 353 padding: 24rpx;
568 border-radius: 12rpx; 354 border-radius: 12rpx;
...@@ -577,89 +363,6 @@ onMounted(() => { ...@@ -577,89 +363,6 @@ onMounted(() => {
577 margin-bottom: 16rpx; 363 margin-bottom: 16rpx;
578 } 364 }
579 365
580 -.taro_html {
581 - // Taro html.css 样式
582 - :deep(.h5-p) {
583 - margin: 16rpx 0;
584 - }
585 -
586 - :deep(.h5-img) {
587 - max-width: 100%;
588 - display: block;
589 - }
590 -
591 - :deep(h1) {
592 - font-size: 48rpx;
593 - font-weight: bold;
594 - margin: 32rpx 0 16rpx;
595 - }
596 -
597 - :deep(h2) {
598 - font-size: 40rpx;
599 - font-weight: bold;
600 - margin: 24rpx 0 12rpx;
601 - }
602 -
603 - :deep(h3) {
604 - font-size: 36rpx;
605 - font-weight: bold;
606 - margin: 20rpx 0 12rpx;
607 - }
608 -
609 - :deep(p) {
610 - font-size: 28rpx;
611 - line-height: 1.6;
612 - margin: 12rpx 0;
613 - color: #333;
614 - }
615 -
616 - :deep(ul) {
617 - padding-left: 48rpx;
618 - margin: 16rpx 0;
619 - }
620 -
621 - :deep(li) {
622 - font-size: 28rpx;
623 - line-height: 1.6;
624 - margin: 8rpx 0;
625 - }
626 -
627 - :deep(a) {
628 - color: #1989fa;
629 - text-decoration: underline;
630 - }
631 -
632 - :deep(strong) {
633 - font-weight: bold;
634 - }
635 -
636 - :deep(em) {
637 - font-style: italic;
638 - }
639 -
640 - :deep(span) {
641 - font-size: 28rpx;
642 - }
643 -
644 - // 文件链接卡片样式
645 - :deep(._file_list) {
646 - display: inline-block !important;
647 - padding: 12px;
648 - border: 1px solid #e1e5e9;
649 - border-radius: 8px;
650 - margin: 8px;
651 - background: #f8f9fa;
652 - box-shadow: 0 2px 4px rgba(0,0,0,0.1);
653 - max-width: 400px;
654 - transition: all 0.3s ease;
655 - text-decoration: none;
656 - color: inherit;
657 - cursor: pointer;
658 - width: 100%;
659 - box-sizing: border-box;
660 - }
661 -}
662 -
663 .test-results { 366 .test-results {
664 background: #fff; 367 background: #fff;
665 padding: 24rpx; 368 padding: 24rpx;
...@@ -717,11 +420,6 @@ onMounted(() => { ...@@ -717,11 +420,6 @@ onMounted(() => {
717 font-weight: bold; 420 font-weight: bold;
718 } 421 }
719 422
720 - &.info {
721 - background: #7232dd;
722 - color: #fff;
723 - }
724 -
725 &.enabled { 423 &.enabled {
726 background: #07c160; 424 background: #07c160;
727 color: #fff; 425 color: #fff;
......