hookehuyr

fix(rich-text): 修复图片自动处理并应用到文章详情页

RichTextRenderer 组件修复:
- 修复 transformElement 初始化顺序问题(在 watch 之前设置)
- 增强图片样式:添加 width:100%!important
- 删除不必要的 onMounted 调用

文章详情页改造:
- 使用 RichTextRenderer 组件替换 <rich-text>
- 删除文章图片列表模块(由组件处理)
- 删除富文本处理逻辑(formatRichText、extractImageUrls 等)
- 删除未使用的 .article-content 类名
- 简化 .divider margin 样式

代码精简:199 行 → 147 行(减少约 26%)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
22 </template> 22 </template>
23 23
24 <script setup> 24 <script setup>
25 -import { ref, watch, onMounted, nextTick } from 'vue' 25 +import { ref, watch, nextTick } from 'vue'
26 import Taro from '@tarojs/taro' 26 import Taro from '@tarojs/taro'
27 import { $ } from '@tarojs/extend' 27 import { $ } from '@tarojs/extend'
28 import { useFileOperation } from '@/composables/useFileOperation' 28 import { useFileOperation } from '@/composables/useFileOperation'
...@@ -149,10 +149,11 @@ const setupTransformElement = () => { ...@@ -149,10 +149,11 @@ const setupTransformElement = () => {
149 return el 149 return el
150 } 150 }
151 151
152 - // 在链接外的 img(内容图片),添加 mode="widthFix" 和 style 152 + // 在链接外的 img(内容图片),添加完整的样式
153 if (el.setAttribute) { 153 if (el.setAttribute) {
154 el.setAttribute('mode', 'widthFix') 154 el.setAttribute('mode', 'widthFix')
155 - el.setAttribute('style', 'width: 100%;') 155 + // 设置完整样式,确保图片宽度100%并保持正确显示
156 + el.setAttribute('style', 'width:100%!important;max-width:100%!important;height:auto!important;display:block;margin:24rpx 0;')
156 } 157 }
157 158
158 return el 159 return el
...@@ -238,6 +239,10 @@ const handleContentChange = () => { ...@@ -238,6 +239,10 @@ const handleContentChange = () => {
238 /** 239 /**
239 * 监听 props 变化 240 * 监听 props 变化
240 */ 241 */
242 +
243 +// 重要:先设置 transformElement,再监听内容变化
244 +setupTransformElement()
245 +
241 watch(() => props.content, handleContentChange, { immediate: true }) 246 watch(() => props.content, handleContentChange, { immediate: true })
242 watch(() => props.enableTransform, () => { 247 watch(() => props.enableTransform, () => {
243 setupTransformElement() 248 setupTransformElement()
...@@ -255,10 +260,7 @@ watch(() => props.enableTransform, () => { ...@@ -255,10 +260,7 @@ watch(() => props.enableTransform, () => {
255 /** 260 /**
256 * 组件挂载 261 * 组件挂载
257 */ 262 */
258 -onMounted(() => { 263 +// transformElement 已在初始化时设置,watch immediate 已处理首次渲染
259 - setupTransformElement()
260 - handleContentChange()
261 -})
262 </script> 264 </script>
263 265
264 <style lang="less" scoped> 266 <style lang="less" scoped>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
18 </view> 18 </view>
19 19
20 <!-- 文章内容 --> 20 <!-- 文章内容 -->
21 - <view v-else-if="article" class="article-content"> 21 + <view v-else-if="article">
22 <!-- 封面图 --> 22 <!-- 封面图 -->
23 <view v-if="article.coverUrl" class="cover-image-wrapper"> 23 <view v-if="article.coverUrl" class="cover-image-wrapper">
24 <image :src="article.coverUrl" mode="widthFix" class="cover-image" /> 24 <image :src="article.coverUrl" mode="widthFix" class="cover-image" />
...@@ -40,24 +40,9 @@ ...@@ -40,24 +40,9 @@
40 <!-- 分割线 --> 40 <!-- 分割线 -->
41 <view class="divider"></view> 41 <view class="divider"></view>
42 42
43 - <!-- 富文本内容 --> 43 + <!-- 富文本内容 - 使用 RichTextRenderer 组件 -->
44 <view class="article-body px-[32rpx]"> 44 <view class="article-body px-[32rpx]">
45 - <rich-text :nodes="article.content" class="rich-text-content" /> 45 + <RichTextRenderer :content="article.content" />
46 - </view>
47 -
48 - <!-- 文章图片列表(点击可预览) -->
49 - <view v-if="imageUrls.length > 0" class="article-images-section px-[32rpx] pb-[32rpx]">
50 - <view class="section-title">文章图片</view>
51 - <view class="image-grid">
52 - <view
53 - v-for="(url, index) in imageUrls"
54 - :key="index"
55 - class="image-item"
56 - @tap="previewImage(url)"
57 - >
58 - <image :src="url" mode="aspectFill" class="thumbnail-image" />
59 - </view>
60 - </view>
61 </view> 46 </view>
62 </view> 47 </view>
63 48
...@@ -99,6 +84,7 @@ ...@@ -99,6 +84,7 @@
99 import { ref, computed } from 'vue' 84 import { ref, computed } from 'vue'
100 import { useLoad } from '@tarojs/taro' 85 import { useLoad } from '@tarojs/taro'
101 import NavHeader from '@/components/navigation/NavHeader.vue' 86 import NavHeader from '@/components/navigation/NavHeader.vue'
87 +import RichTextRenderer from '@/components/RichTextRenderer.vue'
102 import { articleDetailAPI } from '@/api/article' 88 import { articleDetailAPI } from '@/api/article'
103 import { addAPI, delAPI } from '@/api/favorite' 89 import { addAPI, delAPI } from '@/api/favorite'
104 import { mockArticleDetailAPI } from '@/utils/mockData' 90 import { mockArticleDetailAPI } from '@/utils/mockData'
...@@ -140,56 +126,6 @@ const formattedDate = computed(() => { ...@@ -140,56 +126,6 @@ const formattedDate = computed(() => {
140 }) 126 })
141 127
142 /** 128 /**
143 - * 图片 URL 列表(用于预览)
144 - */
145 -const imageUrls = ref([])
146 -
147 -/**
148 - * 预览图片
149 - */
150 -const previewImage = (current) => {
151 - if (imageUrls.value.length > 0) {
152 - Taro.previewImage({
153 - current: current,
154 - urls: imageUrls.value
155 - })
156 - }
157 -}
158 -
159 -/**
160 - * 提取所有图片 URL
161 - */
162 -const extractImageUrls = (content) => {
163 - if (!content) return []
164 - const urls = []
165 - const imgRegex = /<img[^>]+src=["']([^"']+)["']/gi
166 - let match
167 - while ((match = imgRegex.exec(content)) !== null) {
168 - urls.push(match[1])
169 - }
170 - return urls
171 -}
172 -
173 -/**
174 - * 格式化富文本内容,处理图片样式
175 - * 解决移动端图片宽度溢出问题
176 - */
177 -const formatRichText = (html) => {
178 - if (!html) return ''
179 - return html.replace(/<img[^>]*>/gi, (match) => {
180 - // 移除原有的 width 和 height 属性,防止干扰
181 - match = match.replace(/\s(width|height)=["'][^"']*["']/gi, '')
182 -
183 - // 处理 style 属性
184 - if (match.includes('style=')) {
185 - return match.replace(/style=(["'])(.*?)\1/gi, 'style=$1$2;max-width:100%!important;height:auto!important;display:block;margin:24rpx auto;border-radius:12rpx;$1')
186 - } else {
187 - return match.replace(/<img/gi, '<img style="max-width:100%!important;height:auto!important;display:block;margin:24rpx auto;border-radius:12rpx;"')
188 - }
189 - })
190 -}
191 -
192 -/**
193 * 获取文章详情 129 * 获取文章详情
194 */ 130 */
195 const fetchArticleDetail = async () => { 131 const fetchArticleDetail = async () => {
...@@ -199,32 +135,22 @@ const fetchArticleDetail = async () => { ...@@ -199,32 +135,22 @@ const fetchArticleDetail = async () => {
199 error.value = false 135 error.value = false
200 136
201 try { 137 try {
202 - console.log('[Article Detail] 获取文章详情:', articleId.value)
203 - console.log('[Article Detail] 使用 Mock 数据:', USE_MOCK_DATA)
204 -
205 const res = USE_MOCK_DATA 138 const res = USE_MOCK_DATA
206 ? await mockArticleDetailAPI({ i: articleId.value }) 139 ? await mockArticleDetailAPI({ i: articleId.value })
207 : await articleDetailAPI({ i: articleId.value }) 140 : await articleDetailAPI({ i: articleId.value })
208 141
209 if (res.code === 1 && res.data) { 142 if (res.code === 1 && res.data) {
210 - console.log('[Article Detail] 数据:', res.data)
211 -
212 - // 格式化富文本内容,处理图片样式
213 - const content = formatRichText(res.data.post_content || '')
214 -
215 article.value = { 143 article.value = {
216 id: res.data.id, 144 id: res.data.id,
217 title: res.data.post_title || '未命名文章', 145 title: res.data.post_title || '未命名文章',
218 - content: content, 146 + // 直接使用原始内容,由 RichTextRenderer 组件处理
147 + content: res.data.post_content || '',
219 excerpt: res.data.post_excerpt || '', 148 excerpt: res.data.post_excerpt || '',
220 coverUrl: res.data.cover_url || res.data.post_thumbnail || '', 149 coverUrl: res.data.cover_url || res.data.post_thumbnail || '',
221 date: res.data.post_date || '', 150 date: res.data.post_date || '',
222 authorName: res.data.author_name || '', 151 authorName: res.data.author_name || '',
223 is_favorite: res.data.is_favorite === 1 || res.data.is_favorite === '1' 152 is_favorite: res.data.is_favorite === 1 || res.data.is_favorite === '1'
224 } 153 }
225 -
226 - // 提取图片 URL 列表
227 - imageUrls.value = extractImageUrls(content)
228 } else { 154 } else {
229 error.value = true 155 error.value = true
230 Taro.showToast({ 156 Taro.showToast({
...@@ -233,7 +159,6 @@ const fetchArticleDetail = async () => { ...@@ -233,7 +159,6 @@ const fetchArticleDetail = async () => {
233 }) 159 })
234 } 160 }
235 } catch (err) { 161 } catch (err) {
236 - console.error('[Article Detail] 获取文章详情失败:', err)
237 error.value = true 162 error.value = true
238 Taro.showToast({ 163 Taro.showToast({
239 title: '加载失败', 164 title: '加载失败',
...@@ -282,7 +207,6 @@ const toggleCollect = async () => { ...@@ -282,7 +207,6 @@ const toggleCollect = async () => {
282 }) 207 })
283 } 208 }
284 } catch (err) { 209 } catch (err) {
285 - console.error('[Article Detail] 收藏操作失败:', err)
286 Taro.showToast({ 210 Taro.showToast({
287 title: '网络错误,请重试', 211 title: '网络错误,请重试',
288 icon: 'none', 212 icon: 'none',
...@@ -295,8 +219,6 @@ const toggleCollect = async () => { ...@@ -295,8 +219,6 @@ const toggleCollect = async () => {
295 * 页面加载时获取文章详情 219 * 页面加载时获取文章详情
296 */ 220 */
297 useLoad((options) => { 221 useLoad((options) => {
298 - console.log('[Article Detail] 页面参数:', options)
299 -
300 if (options.id) { 222 if (options.id) {
301 articleId.value = options.id 223 articleId.value = options.id
302 fetchArticleDetail() 224 fetchArticleDetail()
...@@ -360,86 +282,13 @@ useLoad((options) => { ...@@ -360,86 +282,13 @@ useLoad((options) => {
360 .divider { 282 .divider {
361 height: 1rpx; 283 height: 1rpx;
362 background-color: #E5E7EB; 284 background-color: #E5E7EB;
363 - margin: 0 0 32rpx 0; 285 + margin-bottom: 32rpx;
364 } 286 }
365 287
366 .article-body { 288 .article-body {
367 padding-bottom: 32rpx; 289 padding-bottom: 32rpx;
368 } 290 }
369 291
370 -.rich-text-content {
371 - font-size: 30rpx;
372 - color: #374151;
373 - line-height: 1.8;
374 - word-wrap: break-word;
375 - overflow: hidden;
376 -
377 - /* 富文本图片样式:确保宽度适配移动端 */
378 - :deep(img) {
379 - max-width: 100% !important;
380 - width: auto !important;
381 - height: auto !important;
382 - display: block !important;
383 - margin: 24rpx auto !important;
384 - border-radius: 12rpx;
385 - }
386 -
387 - /* 标题样式优化 */
388 - :deep(h1),
389 - :deep(h2),
390 - :deep(h3),
391 - :deep(h4),
392 - :deep(h5),
393 - :deep(h6) {
394 - margin: 24rpx 0 16rpx;
395 - font-weight: bold;
396 - color: #1F2937;
397 - }
398 -
399 - :deep(h1) {
400 - font-size: 40rpx;
401 - }
402 -
403 - :deep(h2) {
404 - font-size: 36rpx;
405 - }
406 -
407 - :deep(h3) {
408 - font-size: 32rpx;
409 - }
410 -
411 - /* 段落样式 */
412 - :deep(p) {
413 - margin: 16rpx 0;
414 - }
415 -
416 - /* 列表样式 */
417 - :deep(ul),
418 - :deep(ol) {
419 - padding-left: 32rpx;
420 - margin: 16rpx 0;
421 - }
422 -
423 - :deep(li) {
424 - margin: 8rpx 0;
425 - }
426 -
427 - /* 引用样式 */
428 - :deep(blockquote) {
429 - padding: 16rpx 24rpx;
430 - margin: 16rpx 0;
431 - background-color: #F3F4F6;
432 - border-left: 4rpx solid #D1D5DB;
433 - color: #6B7280;
434 - }
435 -
436 - /* 链接样式 */
437 - :deep(a) {
438 - color: #2563EB;
439 - text-decoration: underline;
440 - }
441 -}
442 -
443 .safe-area-bottom { 292 .safe-area-bottom {
444 height: 120rpx; 293 height: 120rpx;
445 } 294 }
...@@ -500,38 +349,4 @@ useLoad((options) => { ...@@ -500,38 +349,4 @@ useLoad((options) => {
500 align-items: center; 349 align-items: center;
501 min-height: 400rpx; 350 min-height: 400rpx;
502 } 351 }
503 -
504 -/* 文章图片列表区域 - 网格布局 */
505 -.article-images-section {
506 - margin-top: 24rpx;
507 -}
508 -
509 -.section-title {
510 - font-size: 28rpx;
511 - font-weight: bold;
512 - color: #1F2937;
513 - margin-bottom: 16rpx;
514 -}
515 -
516 -.image-grid {
517 - display: flex;
518 - flex-wrap: wrap;
519 - gap: 16rpx;
520 - /* 每行3个图片:计算公式:(750rpx - 64rpx padding - 32rpx gaps) / 3 ≈ 218rpx */
521 -}
522 -
523 -.image-item {
524 - /* 每行3个,宽度约占 1/3 */
525 - width: calc((100% - 32rpx) / 3);
526 - /* 保持正方形 */
527 - aspect-ratio: 1;
528 - border-radius: 12rpx;
529 - overflow: hidden;
530 - background-color: #F3F4F6;
531 -}
532 -
533 -.thumbnail-image {
534 - width: 100%;
535 - height: 100%;
536 -}
537 </style> 352 </style>
......