feat: 医护仪表盘增强 + 患者端文章分类浏览
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- DoctorDashboard 增加 pending_dialysis_review/pending_lab_review/today_appointments
- 医护小程序首页增加「健康审核」区块(待审透析/化验/今日预约)
- 患者端文章列表增加分类 tabs 横向滚动筛选
- article service 增加 listCategories + category_id 筛选
This commit is contained in:
iven
2026-04-26 14:25:06 +08:00
parent c9bf5f6139
commit 7a9054c914
7 changed files with 188 additions and 14 deletions

View File

@@ -7,6 +7,29 @@
padding-bottom: 40px;
}
.article-categories {
white-space: nowrap;
margin-bottom: 24px;
}
.article-cat-tab {
display: inline-block;
padding: 12px 28px;
margin-right: 12px;
font-size: 26px;
color: $tx2;
background: $card;
border-radius: 32px;
border: 2px solid transparent;
&--active {
color: $pri;
background: $pri-l;
border-color: $pri;
font-weight: bold;
}
}
.article-list {
display: flex;
flex-direction: column;

View File

@@ -1,7 +1,7 @@
import React, { useState, useCallback } from 'react';
import { View, Text, Image } from '@tarojs/components';
import React, { useState, useCallback, useEffect } from 'react';
import { View, Text, Image, ScrollView } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
import { listArticles, Article } from '../../services/article';
import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article';
import EmptyState from '../../components/EmptyState';
import Loading from '../../components/Loading';
import './index.scss';
@@ -11,11 +11,26 @@ export default function ArticleList() {
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 fetchData = useCallback(async (p: number, append = false) => {
const fetchCategories = useCallback(async () => {
try {
const data = await listCategories();
setCategories(data || []);
} catch {
// 静默
}
}, []);
const fetchData = useCallback(async (p: number, append = false, categoryId?: string | null) => {
setLoading(true);
try {
const res = await listArticles(p);
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);
@@ -25,14 +40,18 @@ export default function ArticleList() {
} finally {
setLoading(false);
}
}, []);
}, [activeCategory]);
useEffect(() => {
fetchCategories();
}, [fetchCategories]);
useDidShow(() => {
fetchData(1);
fetchData(1, false, null);
});
usePullDownRefresh(() => {
fetchData(1).finally(() => {
fetchData(1, false, null).finally(() => {
Taro.stopPullDownRefresh();
});
});
@@ -43,12 +62,38 @@ export default function ArticleList() {
}
});
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'>
{/* 分类筛选 */}
{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
@@ -62,8 +107,8 @@ export default function ArticleList() {
<Text className='article-card-summary'>{a.summary}</Text>
)}
<View className='article-card-meta'>
{a.category && (
<Text className='article-card-tag'>{a.category}</Text>
{(a.category_name || a.category) && (
<Text className='article-card-tag'>{a.category_name || a.category}</Text>
)}
{a.published_at && (
<Text className='article-card-date'>

View File

@@ -21,6 +21,12 @@ const CARDS: CardConfig[] = [
{ key: 'today_consultations', label: '今日咨询', icon: '🩺', route: '/pages/doctor/consultation/index', color: '#10b981' },
];
const HEALTH_CARDS: CardConfig[] = [
{ key: 'pending_dialysis_review', label: '待审透析', icon: '💧', route: '/pages/doctor/patients/index', color: '#0ea5e9' },
{ key: 'pending_lab_review', label: '待审化验', icon: '🔬', route: '/pages/doctor/report/index', color: '#f43f5e' },
{ key: 'today_appointments', label: '今日预约', icon: '📅', route: '/pages/doctor/patients/index', color: '#14b8a6' },
];
export default function DoctorHome() {
const { user, logout } = useAuthStore();
const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null);
@@ -86,6 +92,24 @@ export default function DoctorHome() {
</View>
</View>
<View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text>
<View className='doctor-home__grid'>
{HEALTH_CARDS.map((card) => (
<View
key={card.key}
className='doctor-home__card'
style={`border-left: 6px solid ${card.color}`}
onClick={() => handleCardClick(card)}
>
<Text className='doctor-home__card-icon'>{card.icon}</Text>
<Text className='doctor-home__card-num'>{getValue(card.key)}</Text>
<Text className='doctor-home__card-label'>{card.label}</Text>
</View>
))}
</View>
</View>
<View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text>
<View className='doctor-home__quick-actions'>

View File

@@ -7,17 +7,40 @@ export interface Article {
content?: string;
cover_image?: string;
category?: string;
category_id?: string;
category_name?: string;
tags?: { id: string; name: string }[];
published_at?: string;
author?: string;
view_count?: number;
}
export async function listArticles(page = 1) {
export interface ArticleCategory {
id: string;
name: string;
parent_id?: string;
children?: ArticleCategory[];
}
export async function listArticles(params?: {
page?: number;
page_size?: number;
category_id?: string;
tag_id?: string;
keyword?: string;
}) {
return api.get<{ data: Article[]; total: number }>('/health/articles', {
page,
page_size: 20,
page: params?.page ?? 1,
page_size: params?.page_size ?? 20,
status: 'published',
...params,
});
}
export async function getArticleDetail(id: string) {
return api.get<Article>(`/health/articles/${id}`);
}
export async function listCategories() {
return api.get<ArticleCategory[]>('/health/article-categories');
}

View File

@@ -8,6 +8,9 @@ export interface DoctorDashboard {
unread_messages: number;
pending_follow_ups: number;
today_consultations: number;
pending_dialysis_review: number;
pending_lab_review: number;
today_appointments: number;
}
export async function getDashboard() {