P1: - 全局 23 个页面 Taro.navigateTo → safeNavigateTo,防止页栈超10层 - 生产构建保留 console.warn/error,便于线上问题排查 - 添加 preloadRule 分包预加载(首页预加载健康/医生/文章分包) P2: - logout 时清理 ai_chat_history + BLE DataBuffer 缓存 - restore() 移除冗余的双重 Storage 读取(secureGet 已包含 getStorageSync) - 首页文章图片添加 lazyLoad
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { View, Text, ScrollView } from '@tarojs/components';
|
|
import Taro from '@tarojs/taro';
|
|
import { safeNavigateTo } from '@/utils/navigate';
|
|
import { usePageData } from '@/hooks/usePageData';
|
|
import { listArticles, listCategories } from '../../services/article';
|
|
import PageShell from '@/components/ui/PageShell';
|
|
import ContentCard from '@/components/ui/ContentCard';
|
|
import LoadingCard from '@/components/ui/LoadingCard';
|
|
import EmptyState from '@/components/EmptyState';
|
|
import ErrorState from '@/components/ErrorState';
|
|
import Loading from '@/components/Loading';
|
|
import { useElderClass } from '../../hooks/useElderClass';
|
|
import './index.scss';
|
|
|
|
interface ArticleItem {
|
|
id: string;
|
|
title: string;
|
|
summary?: string;
|
|
cover_image?: string;
|
|
category?: string;
|
|
category_id?: string;
|
|
category_name?: string;
|
|
published_at?: string;
|
|
status?: string;
|
|
}
|
|
|
|
interface ArticleCategory {
|
|
id: string;
|
|
name: string;
|
|
parent_id?: string | null;
|
|
}
|
|
|
|
export default function ArticleList() {
|
|
const modeClass = useElderClass();
|
|
const [articles, setArticles] = useState<ArticleItem[]>([]);
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(false);
|
|
const [categories, setCategories] = useState<ArticleCategory[]>([]);
|
|
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
|
|
|
const fetchData = useCallback(async (p: number, append = false, categoryId?: string | null) => {
|
|
setLoading(true);
|
|
setError(false);
|
|
try {
|
|
const cid = categoryId !== undefined ? categoryId : activeCategory;
|
|
const res = await listArticles({
|
|
page: p,
|
|
category_id: cid || undefined,
|
|
});
|
|
const list = res.data || [];
|
|
setArticles(append ? (prev) => [...prev, ...list] : list);
|
|
setTotal(res.total);
|
|
setPage(p);
|
|
} catch {
|
|
setError(true);
|
|
Taro.showToast({ title: '加载失败', icon: 'none' });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [activeCategory]);
|
|
|
|
usePageData(
|
|
useCallback(async () => {
|
|
try {
|
|
const cats = await listCategories();
|
|
setCategories(cats || []);
|
|
} catch {
|
|
setCategories([]);
|
|
}
|
|
await fetchData(1);
|
|
}, [fetchData]),
|
|
{ throttleMs: 10000, enablePullDown: true },
|
|
);
|
|
|
|
const handleCategoryChange = (categoryId: string | null) => {
|
|
setActiveCategory(categoryId);
|
|
fetchData(1, false, categoryId);
|
|
};
|
|
|
|
const goToDetail = (id: string) => {
|
|
safeNavigateTo(`/pages/article/detail/index?id=${id}`);
|
|
};
|
|
|
|
if (loading && articles.length === 0 && !error) {
|
|
return <LoadingCard count={3} />;
|
|
}
|
|
|
|
return (
|
|
<PageShell safeBottom padding="none" className={modeClass}>
|
|
{categories.length > 0 && (
|
|
<ScrollView scrollX className='article-categories'>
|
|
<View
|
|
className={`article-cat-tab ${!activeCategory ? 'article-cat-tab--active' : ''}`}
|
|
onClick={() => handleCategoryChange(null)}
|
|
>
|
|
<Text>全部</Text>
|
|
</View>
|
|
{categories.map((cat) => (
|
|
<View
|
|
key={cat.id}
|
|
className={`article-cat-tab ${activeCategory === cat.id ? 'article-cat-tab--active' : ''}`}
|
|
onClick={() => handleCategoryChange(cat.id)}
|
|
>
|
|
<Text>{cat.name}</Text>
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
)}
|
|
|
|
{error ? (
|
|
<ErrorState onRetry={() => fetchData(1, false, null)} />
|
|
) : articles.length === 0 && !loading ? (
|
|
<EmptyState text='暂无资讯文章' />
|
|
) : (
|
|
<View className='article-list'>
|
|
{articles.map((a) => (
|
|
<ContentCard
|
|
key={a.id}
|
|
padding="sm"
|
|
margin="none"
|
|
onPress={() => goToDetail(a.id)}
|
|
>
|
|
<View className='article-card-body'>
|
|
<Text className='article-card-title'>{a.title}</Text>
|
|
{a.summary && (
|
|
<Text className='article-card-summary'>{a.summary}</Text>
|
|
)}
|
|
<View className='article-card-meta'>
|
|
{(a.category_name || a.category) && (
|
|
<Text className='article-card-tag'>{a.category_name || a.category}</Text>
|
|
)}
|
|
{a.published_at && (
|
|
<Text className='article-card-date'>
|
|
{a.published_at.slice(0, 10)}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</ContentCard>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{loading && <Loading />}
|
|
</PageShell>
|
|
);
|
|
}
|