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>
Showing
2 changed files
with
16 additions
and
199 deletions
| ... | @@ -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> | ... | ... |
-
Please register or login to post a comment