feat: 医护仪表盘增强 + 患者端文章分类浏览
- DoctorDashboard 增加 pending_dialysis_review/pending_lab_review/today_appointments - 医护小程序首页增加「健康审核」区块(待审透析/化验/今日预约) - 患者端文章列表增加分类 tabs 横向滚动筛选 - article service 增加 listCategories + category_id 筛选
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -244,9 +244,13 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.list")?;
|
||||
let result = consultation_service::get_doctor_dashboard(
|
||||
let mut result = consultation_service::get_doctor_dashboard(
|
||||
&state, ctx.tenant_id, ctx.user_id,
|
||||
)
|
||||
.await?;
|
||||
consultation_service::enrich_doctor_dashboard_health(
|
||||
&state, ctx.tenant_id, ctx.user_id, &mut result,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
@@ -426,6 +426,9 @@ pub struct DoctorDashboard {
|
||||
pub unread_messages: i64,
|
||||
pub pending_follow_ups: i64,
|
||||
pub today_consultations: i64,
|
||||
pub pending_dialysis_review: i64,
|
||||
pub pending_lab_review: i64,
|
||||
pub today_appointments: i64,
|
||||
}
|
||||
|
||||
/// 获取指定医生的仪表盘数据。
|
||||
@@ -456,6 +459,9 @@ pub async fn get_doctor_dashboard(
|
||||
unread_messages: 0,
|
||||
pending_follow_ups: 0,
|
||||
today_consultations: 0,
|
||||
pending_dialysis_review: 0,
|
||||
pending_lab_review: 0,
|
||||
today_appointments: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -525,5 +531,51 @@ pub async fn get_doctor_dashboard(
|
||||
unread_messages,
|
||||
pending_follow_ups: pending_follow_ups as i64,
|
||||
today_consultations: today_consultations as i64,
|
||||
pending_dialysis_review: 0,
|
||||
pending_lab_review: 0,
|
||||
today_appointments: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// 补充医生仪表盘中的健康数据计数。
|
||||
/// 由 consultation_service 调用,因为 DoctorDashboard 定义在此模块。
|
||||
pub async fn enrich_doctor_dashboard_health(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
doctor_user_id: Uuid,
|
||||
dashboard: &mut DoctorDashboard,
|
||||
) -> HealthResult<()> {
|
||||
use crate::entity::{dialysis_record, lab_report, appointment};
|
||||
|
||||
// 待审核透析记录(doctor_id 通过患者关联,这里取全租户待审核)
|
||||
let pending_dialysis = dialysis_record::Entity::find()
|
||||
.filter(dialysis_record::Column::TenantId.eq(tenant_id))
|
||||
.filter(dialysis_record::Column::DeletedAt.is_null())
|
||||
.filter(dialysis_record::Column::Status.eq("draft"))
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
dashboard.pending_dialysis_review = pending_dialysis as i64;
|
||||
|
||||
// 待审核化验报告
|
||||
let pending_lab = lab_report::Entity::find()
|
||||
.filter(lab_report::Column::TenantId.eq(tenant_id))
|
||||
.filter(lab_report::Column::DeletedAt.is_null())
|
||||
.filter(lab_report::Column::Status.eq("pending"))
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
dashboard.pending_lab_review = pending_lab as i64;
|
||||
|
||||
// 今日预约
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let today_appts = appointment::Entity::find()
|
||||
.filter(appointment::Column::TenantId.eq(tenant_id))
|
||||
.filter(appointment::Column::DeletedAt.is_null())
|
||||
.filter(appointment::Column::AppointmentDate.eq(today))
|
||||
.filter(appointment::Column::DoctorId.eq(doctor_user_id))
|
||||
.filter(appointment::Column::Status.is_in(["confirmed", "pending"]))
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
dashboard.today_appointments = today_appts as i64;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user