hookehuyr

feat(article): 文章详情页添加图片预览功能

- 新增文章详情页面,支持富文本内容渲染
- 实现图片列表展示,支持点击预览所有文章图片
- 使用 Taro 原生 rich-text 组件渲染富文本
- 富文本内容自动格式化,处理图片宽度适配移动端
- 提取文章中的图片 URL,支持 Taro.previewImage 预览
- 新增收藏功能,支持文章收藏/取消收藏

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -100,3 +100,94 @@
**变更摘要**:
- 无详细描述
### 12:57:01 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
### 12:58:11 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
### 13:00:59 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
### 13:01:30 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
### 13:02:31 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
### 13:03:07 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
### 13:03:33 - 完成任务
**影响文件**:
- `docs/api-specs/article/article_detail.md`
- `docs/api-specs/article/favorite.md`
- `docs/api-specs/article/list.md`
- `docs/api-specs/article/week_hot.md`
- `scripts/api-generator/generateApiFromOpenAPI.js`
- `src/api/article.js`
**变更摘要**:
- 无详细描述
......
......@@ -61,6 +61,8 @@ pnpm lint
- **Git 工作流标准化** - 使用 standard-version + Conventional Commits
- **认证系统完善** - 401 自动刷新、登录权限检查、TabBar 红点
- **API 集成进度** - 29 个接口,已完成 26 个(89.7%)
- **文章详情优化** - 富文本图片点击可预览
- **文章详情修复** - 富文本图片点击事件稳定识别
## ⚡ 常见问题
......
......@@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
AgePickerGlobal: typeof import('./src/components/plan/PlanFields/AgePickerGlobal.vue')['default']
AmountKeyboard: typeof import('./src/components/plan/PlanFields/AmountKeyboard.vue')['default']
ArticleCard: typeof import('./src/components/cards/ArticleCard.vue')['default']
CriticalIllnessTemplate: typeof import('./src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue')['default']
DatePickerGlobal: typeof import('./src/components/plan/PlanFields/DatePickerGlobal.vue')['default']
DocumentPreview: typeof import('./src/components/documents/DocumentPreview/index.vue')['default']
......
# CHANGELOG
## [2026-02-27] - 文章详情富文本样式优化
### 修复
- 修复富文本内容中图片宽度溢出的问题:自动给 img 标签添加 max-width: 100% 及相关样式
---
## [2026-02-27] - 文章详情富文本图片预览
### 修复
- 富文本图片点击可预览,支持 data-index 与 src 兜底识别
---
## [2026-02-27] - 文章详情富文本图片点击修复
### 修复
- 使用 rich-text 点击事件读取 node 信息触发图片预览
---
## [2026-02-27] - 更新 CHANGELOG - feat(article): 文章模块功能开发
### 文档
- ����
---
**详细信息**
- **影响文件**: 无
- **技术栈**: Taro 4, Vue 3, NutUI
- **测试状态**: 待验证
- **备注**: 自动生成
## [2026-02-27] - 文章详情页修复与优化
### 修复
......
......@@ -87,6 +87,7 @@
"@vue/babel-plugin-jsx": "^1.0.6",
"@vue/compiler-sfc": "^3.0.0",
"@vue/test-utils": "^2.4.6",
"ajv": "^8.17.1",
"autoprefixer": "^10.4.21",
"babel-preset-taro": "4.1.11",
"css-loader": "3.4.2",
......@@ -97,7 +98,6 @@
"eslint-plugin-vue": "^8.0.0",
"happy-dom": "^14.12.0",
"husky": "^9.1.7",
"ajv": "^8.17.1",
"js-yaml": "^4.1.1",
"less": "^4.2.0",
"lint-staged": "^16.2.7",
......
......@@ -24,28 +24,9 @@ const Api = {
post_date: string; // 发布日期
post_author: integer; // 发布人id
author_name: string; // 发布人
file_list: {
icon: {
meta_type: string; //
id: integer; //
object_id: null; //
name: string; //
value: string; //
description: null; //
extension: string; //
post_date: string; //
icon: null; //
master_client_id: null; //
hash: string; //
height: string; //
width: string; //
author: integer; //
size: null; //
};
};
is_favorite: integer; //
* };
* }>}
* } >}
*/
export const articleDetailAPI = (params) => fn(fetch.get(Api.ArticleDetail, params));
......
......@@ -31,6 +31,8 @@ const pages = [
'pages/message/index',
'pages/message-detail/index',
'pages/video-player/index',
'pages/article-detail/index',
'pages/article-favorites/index',
]
if (process.env.NODE_ENV === 'development') {
......
/*
* @Date: 2026-02-13 01:05:52
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-27 10:22:51
* @LastEditTime: 2026-02-27 13:17:37
* @FilePath: /manulife-weapp/src/config/app.js
* @Description: 应用配置
*/
......
......@@ -42,7 +42,24 @@
<!-- 富文本内容 -->
<view class="article-body px-[32rpx]">
<rich-text :nodes="processedContent" class="rich-text-content" />
<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>
<scroll-view scroll-x class="image-scroll-view">
<view class="image-list">
<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>
</scroll-view>
</view>
</view>
......@@ -125,13 +142,54 @@ const formattedDate = computed(() => {
})
/**
* 处理富文本内容,适配图片宽度
* 图片 URL 列表(用于预览)
*/
const processedContent = computed(() => {
if (!article.value?.content) return ''
// 给 img 标签添加内联样式,确保宽度不超过容器
return article.value.content.replace(/<img/gi, '<img style="max-width:100%;height:auto;display:block;margin:12px auto;"')
})
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;"')
}
})
}
/**
* 获取文章详情
......@@ -153,16 +211,22 @@ const fetchArticleDetail = async () => {
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: res.data.post_content || '',
content: 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({
......@@ -230,14 +294,6 @@ const toggleCollect = async () => {
}
/**
* 滚动到底部事件
*/
const onScrollToLower = () => {
// 可以在这里加载相关文章推荐等
console.log('[Article Detail] 滚动到底部')
}
/**
* 页面加载时获取文章详情
*/
useLoad((options) => {
......@@ -446,4 +502,39 @@ 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-scroll-view {
white-space: nowrap;
}
.image-list {
display: inline-flex;
gap: 16rpx;
}
.image-item {
flex-shrink: 0;
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
background-color: #F3F4F6;
}
.thumbnail-image {
width: 100%;
height: 100%;
}
</style>
......
This diff is collapsed. Click to expand it.
......@@ -155,11 +155,20 @@ const rawMenuItems = [
iconColor: '#059669', // Emerald (Trust)
bgClass: 'bg-emerald-50'
},
// 原有的"我的收藏"(文件收藏)暂时屏蔽
// {
// key: 'favorites',
// title: '我的收藏',
// icon: 'star',
// path: '/pages/favorites/index',
// iconColor: '#D97706',
// bgClass: 'bg-amber-50'
// },
{
key: 'favorites',
title: '我的收藏',
key: 'article-favorites',
title: '我的收藏文章',
icon: 'star',
path: '/pages/favorites/index',
path: '/pages/article-favorites/index',
iconColor: '#D97706', // Amber (Value)
bgClass: 'bg-amber-50'
},
......
<!--
* @Date: 2026-02-08
* @Description: 本周热门资料页 - 使用 LoadMoreList 组件重构版本
* @Date: 2026-02-27
* @Description: 本周热门文章页 - 改造版(原热门资料页)
-->
<template>
<LoadMoreList
......@@ -10,27 +10,26 @@
:has-more="hasMore"
:loading="loading"
:loading-more="loadingMore"
key-field="meta_id"
key-field="id"
:has-footer="false"
@load-more="handleLoadMore"
>
<!-- 头部 -->
<template #header>
<NavHeader title="本周热门资料" />
<NavHeader title="本周热门文章" />
</template>
<!-- 列表项 -->
<template #item="{ item }">
<MaterialCard
:id="item.meta_id"
:title="item.name"
:file-name="item.name"
:file-size="item.size"
<ArticleCard
:id="item.id"
:title="item.title"
:excerpt="item.excerpt"
:cover-url="item.coverUrl"
:date="item.date"
:learners="item.learners"
:read-people-percent="item.read_people_percent"
:read-people-percent="item.readPeoplePercent"
:collected="item.collected"
:extension="item.extension"
:download-url="item.downloadUrl"
@collect-changed="handleCollectChanged(item, $event)"
/>
</template>
......@@ -42,9 +41,9 @@ import { ref } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import LoadMoreList from '@/components/list/LoadMoreList'
import NavHeader from '@/components/navigation/NavHeader.vue'
import MaterialCard from '@/components/cards/MaterialCard.vue'
import { weekHotAPI } from '@/api/file'
import { mockWeekHotAPI } from '@/utils/mockData'
import ArticleCard from '@/components/cards/ArticleCard.vue'
import { weekHotAPI } from '@/api/article'
import { mockArticleWeekHotAPI } from '@/utils/mockData'
import { USE_MOCK_DATA } from '@/config/app'
// ⚠️ MOCK 数据开关 - 统一从 @/config/app 导入
......@@ -90,20 +89,20 @@ const loadingMore = ref(false)
* 处理收藏状态改变
*
* @description 当用户点击收藏按钮时,更新本地状态
* @param {Object} item - 资料对象
* @param {Object} item - 文章对象
* @param {Object} newStatus - 新的状态 { collected: boolean }
*/
const handleCollectChanged = (item, newStatus) => {
console.log('[Week Hot] 收藏状态改变:', item.name, newStatus.collected)
console.log('[Week Hot] 收藏状态改变:', item.title, newStatus.collected)
// 找到对应的项并更新状态
const material = currentList.value.find(m => m.meta_id === item.meta_id)
if (material) {
material.collected = newStatus.collected
const article = currentList.value.find(a => a.id === item.id)
if (article) {
article.collected = newStatus.collected
}
}
/**
* 获取本周热门资料列表
* 获取本周热门文章列表
*
* @param {Object} params - 请求参数
* @param {number} params.page - 页码(从0开始)
......@@ -125,7 +124,7 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockWeekHotAPI(params)
? await mockArticleWeekHotAPI(params)
: await weekHotAPI(params)
if (res.code === 1 && res.data) {
......@@ -133,19 +132,17 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
// 处理列表数据
if (res.data.list?.length) {
// 直接映射为 MaterialCard 需要的格式
// 不手动提取 extension,让 MaterialCard 内部使用 extractExtensionFromFile 自动从 URL 解析
const listData = res.data.list.map(item => {
return {
meta_id: item.meta_id,
name: item.name || '未命名文件',
size: item.size || '',
downloadUrl: item.src,
collected: item.is_favorite === '1' || item.is_favorite === 1 || item.is_favorite === true,
read_people_count: item.read_people_count,
read_people_percent: item.read_people_percent
}
})
// 映射为 ArticleCard 需要的格式
const listData = res.data.list.map(item => ({
id: item.id,
title: item.post_title || '未命名文章',
excerpt: item.post_excerpt || '',
coverUrl: item.file_list?.icon?.value || '',
date: item.post_date || '',
collected: item.is_favorite === 1 || item.is_favorite === '1' || item.is_favorite === true,
learners: item.read_people_count,
readPeoplePercent: item.read_people_percent
}))
if (isLoadMore) {
// 加载更多:追加数据
......@@ -168,13 +165,13 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
}
} else {
Taro.showToast({
title: res.msg || '获取热门资料失败',
title: res.msg || '获取热门文章失败',
icon: 'none',
duration: 2000
})
}
} catch (error) {
console.error('[Week Hot] 获取热门资料失败:', error)
console.error('[Week Hot] 获取热门文章失败:', error)
Taro.showToast({
title: '加载失败',
icon: 'error',
......@@ -199,7 +196,7 @@ useLoad(async (options) => {
currentPage.value = 0
hasMore.value = true
// 获取本周热门资料列表
// 获取本周热门文章列表
await fetchWeekHotList({ page: 0, limit: pageSize })
})
......
This diff is collapsed. Click to expand it.