T40 UI 审计修复(60 页面全覆盖): - 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码 - 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件) - 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页) - 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加) - statusTag 移除硬编码布局值,改用 SCSS mixin 控制 - 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect) - 移除 action-inbox 未使用 import 安全 P0 修复: - JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化 - 速率限制增强:滑动窗口 + 暴力破解防护 - analytics handler 错误处理完善 文档: - T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK) - 5 份 DevTools/性能审计讨论记录 - wiki 症状导航 + 小程序章节更新
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { View, Text, Image, ScrollView } from '@tarojs/components';
|
|
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
|
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
|
import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article';
|
|
import EmptyState from '../../components/EmptyState';
|
|
import Loading from '../../components/Loading';
|
|
import { useElderClass } from '../../hooks/useElderClass';
|
|
import './index.scss';
|
|
|
|
export default function ArticleList() {
|
|
const modeClass = useElderClass();
|
|
const [articles, setArticles] = useState<Article[]>([]);
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [categories, setCategories] = useState<ArticleCategory[]>([]);
|
|
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
|
|
|
const fetchCategories = useCallback(async () => {
|
|
try {
|
|
const data = await listCategories();
|
|
setCategories(data || []);
|
|
} catch {
|
|
setCategories([]);
|
|
}
|
|
}, []);
|
|
|
|
const fetchData = useCallback(async (p: number, append = false, categoryId?: string | null) => {
|
|
setLoading(true);
|
|
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 {
|
|
Taro.showToast({ title: '加载失败', icon: 'none' });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [activeCategory]);
|
|
|
|
useEffect(() => {
|
|
fetchCategories();
|
|
}, [fetchCategories]);
|
|
|
|
useThrottledDidShow(() => {
|
|
fetchData(1, false, null);
|
|
}, 10000);
|
|
|
|
usePullDownRefresh(() => {
|
|
fetchData(1, false, null).finally(() => {
|
|
Taro.stopPullDownRefresh();
|
|
});
|
|
});
|
|
|
|
useReachBottom(() => {
|
|
if (!loading && articles.length < total) {
|
|
fetchData(page + 1, true);
|
|
}
|
|
});
|
|
|
|
const handleCategoryChange = (categoryId: string | null) => {
|
|
setActiveCategory(categoryId);
|
|
fetchData(1, false, categoryId);
|
|
};
|
|
|
|
const goToDetail = (id: string) => {
|
|
Taro.navigateTo({ url: `/pages/article/detail/index?id=${id}` });
|
|
};
|
|
|
|
return (
|
|
<View className={`article-page ${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>
|
|
)}
|
|
|
|
<View className='article-list'>
|
|
{articles.map((a) => (
|
|
<View
|
|
className='article-card'
|
|
key={a.id}
|
|
onClick={() => 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>
|
|
{a.cover_image && (
|
|
<View className='article-card-cover'>
|
|
<Image className='cover-img' src={a.cover_image} mode='aspectFill' lazyLoad />
|
|
</View>
|
|
)}
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
{articles.length === 0 && !loading && (
|
|
<EmptyState text='暂无资讯文章' />
|
|
)}
|
|
|
|
{loading && (
|
|
<Loading />
|
|
)}
|
|
</View>
|
|
);
|
|
}
|