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 @@
</template>
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import { ref, watch, nextTick } from 'vue'
import Taro from '@tarojs/taro'
import { $ } from '@tarojs/extend'
import { useFileOperation } from '@/composables/useFileOperation'
......@@ -149,10 +149,11 @@ const setupTransformElement = () => {
return el
}
// 在链接外的 img(内容图片),添加 mode="widthFix" 和 style
// 在链接外的 img(内容图片),添加完整的样式
if (el.setAttribute) {
el.setAttribute('mode', 'widthFix')
el.setAttribute('style', 'width: 100%;')
// 设置完整样式,确保图片宽度100%并保持正确显示
el.setAttribute('style', 'width:100%!important;max-width:100%!important;height:auto!important;display:block;margin:24rpx 0;')
}
return el
......@@ -238,6 +239,10 @@ const handleContentChange = () => {
/**
* 监听 props 变化
*/
// 重要:先设置 transformElement,再监听内容变化
setupTransformElement()
watch(() => props.content, handleContentChange, { immediate: true })
watch(() => props.enableTransform, () => {
setupTransformElement()
......@@ -255,10 +260,7 @@ watch(() => props.enableTransform, () => {
/**
* 组件挂载
*/
onMounted(() => {
setupTransformElement()
handleContentChange()
})
// transformElement 已在初始化时设置,watch immediate 已处理首次渲染
</script>
<style lang="less" scoped>
......
......@@ -18,7 +18,7 @@
</view>
<!-- 文章内容 -->
<view v-else-if="article" class="article-content">
<view v-else-if="article">
<!-- 封面图 -->
<view v-if="article.coverUrl" class="cover-image-wrapper">
<image :src="article.coverUrl" mode="widthFix" class="cover-image" />
......@@ -40,24 +40,9 @@
<!-- 分割线 -->
<view class="divider"></view>
<!-- 富文本内容 -->
<!-- 富文本内容 - 使用 RichTextRenderer 组件 -->
<view class="article-body px-[32rpx]">
<rich-text :nodes="article.content" class="rich-text-content" />
</view>
<!-- 文章图片列表(点击可预览) -->
<view v-if="imageUrls.length > 0" class="article-images-section px-[32rpx] pb-[32rpx]">
<view class="section-title">文章图片</view>
<view class="image-grid">
<view
v-for="(url, index) in imageUrls"
:key="index"
class="image-item"
@tap="previewImage(url)"
>
<image :src="url" mode="aspectFill" class="thumbnail-image" />
</view>
</view>
<RichTextRenderer :content="article.content" />
</view>
</view>
......@@ -99,6 +84,7 @@
import { ref, computed } from 'vue'
import { useLoad } from '@tarojs/taro'
import NavHeader from '@/components/navigation/NavHeader.vue'
import RichTextRenderer from '@/components/RichTextRenderer.vue'
import { articleDetailAPI } from '@/api/article'
import { addAPI, delAPI } from '@/api/favorite'
import { mockArticleDetailAPI } from '@/utils/mockData'
......@@ -140,56 +126,6 @@ const formattedDate = computed(() => {
})
/**
* 图片 URL 列表(用于预览)
*/
const imageUrls = ref([])
/**
* 预览图片
*/
const previewImage = (current) => {
if (imageUrls.value.length > 0) {
Taro.previewImage({
current: current,
urls: imageUrls.value
})
}
}
/**
* 提取所有图片 URL
*/
const extractImageUrls = (content) => {
if (!content) return []
const urls = []
const imgRegex = /<img[^>]+src=["']([^"']+)["']/gi
let match
while ((match = imgRegex.exec(content)) !== null) {
urls.push(match[1])
}
return urls
}
/**
* 格式化富文本内容,处理图片样式
* 解决移动端图片宽度溢出问题
*/
const formatRichText = (html) => {
if (!html) return ''
return html.replace(/<img[^>]*>/gi, (match) => {
// 移除原有的 width 和 height 属性,防止干扰
match = match.replace(/\s(width|height)=["'][^"']*["']/gi, '')
// 处理 style 属性
if (match.includes('style=')) {
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')
} else {
return match.replace(/<img/gi, '<img style="max-width:100%!important;height:auto!important;display:block;margin:24rpx auto;border-radius:12rpx;"')
}
})
}
/**
* 获取文章详情
*/
const fetchArticleDetail = async () => {
......@@ -199,32 +135,22 @@ const fetchArticleDetail = async () => {
error.value = false
try {
console.log('[Article Detail] 获取文章详情:', articleId.value)
console.log('[Article Detail] 使用 Mock 数据:', USE_MOCK_DATA)
const res = USE_MOCK_DATA
? await mockArticleDetailAPI({ i: articleId.value })
: await articleDetailAPI({ i: articleId.value })
if (res.code === 1 && res.data) {
console.log('[Article Detail] 数据:', res.data)
// 格式化富文本内容,处理图片样式
const content = formatRichText(res.data.post_content || '')
article.value = {
id: res.data.id,
title: res.data.post_title || '未命名文章',
content: content,
// 直接使用原始内容,由 RichTextRenderer 组件处理
content: res.data.post_content || '',
excerpt: res.data.post_excerpt || '',
coverUrl: res.data.cover_url || res.data.post_thumbnail || '',
date: res.data.post_date || '',
authorName: res.data.author_name || '',
is_favorite: res.data.is_favorite === 1 || res.data.is_favorite === '1'
}
// 提取图片 URL 列表
imageUrls.value = extractImageUrls(content)
} else {
error.value = true
Taro.showToast({
......@@ -233,7 +159,6 @@ const fetchArticleDetail = async () => {
})
}
} catch (err) {
console.error('[Article Detail] 获取文章详情失败:', err)
error.value = true
Taro.showToast({
title: '加载失败',
......@@ -282,7 +207,6 @@ const toggleCollect = async () => {
})
}
} catch (err) {
console.error('[Article Detail] 收藏操作失败:', err)
Taro.showToast({
title: '网络错误,请重试',
icon: 'none',
......@@ -295,8 +219,6 @@ const toggleCollect = async () => {
* 页面加载时获取文章详情
*/
useLoad((options) => {
console.log('[Article Detail] 页面参数:', options)
if (options.id) {
articleId.value = options.id
fetchArticleDetail()
......@@ -360,86 +282,13 @@ useLoad((options) => {
.divider {
height: 1rpx;
background-color: #E5E7EB;
margin: 0 0 32rpx 0;
margin-bottom: 32rpx;
}
.article-body {
padding-bottom: 32rpx;
}
.rich-text-content {
font-size: 30rpx;
color: #374151;
line-height: 1.8;
word-wrap: break-word;
overflow: hidden;
/* 富文本图片样式:确保宽度适配移动端 */
:deep(img) {
max-width: 100% !important;
width: auto !important;
height: auto !important;
display: block !important;
margin: 24rpx auto !important;
border-radius: 12rpx;
}
/* 标题样式优化 */
:deep(h1),
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
margin: 24rpx 0 16rpx;
font-weight: bold;
color: #1F2937;
}
:deep(h1) {
font-size: 40rpx;
}
:deep(h2) {
font-size: 36rpx;
}
:deep(h3) {
font-size: 32rpx;
}
/* 段落样式 */
:deep(p) {
margin: 16rpx 0;
}
/* 列表样式 */
:deep(ul),
:deep(ol) {
padding-left: 32rpx;
margin: 16rpx 0;
}
:deep(li) {
margin: 8rpx 0;
}
/* 引用样式 */
:deep(blockquote) {
padding: 16rpx 24rpx;
margin: 16rpx 0;
background-color: #F3F4F6;
border-left: 4rpx solid #D1D5DB;
color: #6B7280;
}
/* 链接样式 */
:deep(a) {
color: #2563EB;
text-decoration: underline;
}
}
.safe-area-bottom {
height: 120rpx;
}
......@@ -500,38 +349,4 @@ useLoad((options) => {
align-items: center;
min-height: 400rpx;
}
/* 文章图片列表区域 - 网格布局 */
.article-images-section {
margin-top: 24rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 16rpx;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
/* 每行3个图片:计算公式:(750rpx - 64rpx padding - 32rpx gaps) / 3 ≈ 218rpx */
}
.image-item {
/* 每行3个,宽度约占 1/3 */
width: calc((100% - 32rpx) / 3);
/* 保持正方形 */
aspect-ratio: 1;
border-radius: 12rpx;
overflow: hidden;
background-color: #F3F4F6;
}
.thumbnail-image {
width: 100%;
height: 100%;
}
</style>
......