Files
hms/apps/miniprogram/src/pages/article/detail/index.tsx
iven 185f411495 feat(mp): 文章详情页改用 mp-html 原生富文本组件
- 引入 mp-html 替代 RichText,支持图文混排、表格等复杂内容
- 新建 RichArticle 组件封装 sanitizeHtml + mp-html
- 通过 native-components 拷贝原生组件到 dist
- 优化文章排版样式(字号、间距、分隔线、底栏安全区)
- sanitize-html 扩展允许 style/data-w-e-type 属性
2026-05-25 13:44:00 +08:00

113 lines
3.4 KiB
TypeScript

import { useState, useCallback } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { getArticleDetail, getPublicArticleDetail, Article } from '../../../services/article';
import { trackEvent } from '@/services/analytics';
import { useElderClass } from '../../../hooks/useElderClass';
import { useAuthStore } from '../../../stores/auth';
import PageShell from '@/components/ui/PageShell';
import RichArticle from '@/components/RichArticle';
import './index.scss';
export default function ArticleDetail() {
const modeClass = useElderClass();
const router = useRouter();
const id = router.params.id || '';
const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState(true);
useShareAppMessage(() => {
trackEvent('article_share', { article_id: id });
return {
title: article?.title || '健康资讯',
path: `/pages/article/detail/index?id=${id}`,
};
});
const fetchArticle = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const user = useAuthStore.getState().user;
const fetcher = user ? getArticleDetail(id) : getPublicArticleDetail(id);
const data = await fetcher;
setArticle(data);
} catch (err) {
console.warn('[article] 加载文章详情失败:', err);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
}, [id]);
usePageData(fetchArticle, { throttleMs: 60000 });
if (loading) {
return (
<PageShell className={modeClass}>
<View className='loading-state'>
<Text className='loading-text'>...</Text>
</View>
</PageShell>
);
}
if (!article) {
return (
<PageShell className={modeClass}>
<View className='empty-state'>
<Text className='empty-text'></Text>
</View>
</PageShell>
);
}
const handleCollect = () => {
Taro.showToast({ title: '已收藏', icon: 'success' });
};
const handleShare = () => {
Taro.showShareMenu({ withShareTicket: true });
};
return (
<PageShell padding="md" safeBottom={false} scroll={false} className={`article-detail-page ${modeClass}`}>
<Text className='article-title'>{article.title}</Text>
<View className='article-meta'>
{article.author && (
<View className='meta-item'>
<Text className='meta-icon'></Text>
<Text>{article.author}</Text>
</View>
)}
{article.published_at && (
<View className='meta-item'>
<Text className='meta-icon'>📅</Text>
<Text>{article.published_at.slice(0, 10)}</Text>
</View>
)}
</View>
<View className='article-divider' />
<View className='article-body'>
<RichArticle html={article.content || ''} />
</View>
<View className='article-bottom-bar'>
<View className='article-action-btn' onClick={handleCollect}>
<Text className='article-action-icon'></Text>
<Text className='article-action-text'></Text>
</View>
<View className='article-action-btn' onClick={handleShare}>
<Text className='article-action-icon'></Text>
<Text className='article-action-text'></Text>
</View>
</View>
</PageShell>
);
}