From 7a9054c9143f5b8b4aa6bec5ba7018872a8ef05a Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 26 Apr 2026 14:25:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8C=BB=E6=8A=A4=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E5=A2=9E=E5=BC=BA=20+=20=E6=82=A3=E8=80=85=E7=AB=AF?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E5=88=86=E7=B1=BB=E6=B5=8F=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DoctorDashboard 增加 pending_dialysis_review/pending_lab_review/today_appointments - 医护小程序首页增加「健康审核」区块(待审透析/化验/今日预约) - 患者端文章列表增加分类 tabs 横向滚动筛选 - article service 增加 listCategories + category_id 筛选 --- apps/miniprogram/src/pages/article/index.scss | 23 +++++++ apps/miniprogram/src/pages/article/index.tsx | 65 ++++++++++++++++--- apps/miniprogram/src/pages/doctor/index.tsx | 24 +++++++ apps/miniprogram/src/services/article.ts | 29 ++++++++- apps/miniprogram/src/services/doctor.ts | 3 + .../src/handler/consultation_handler.rs | 6 +- .../src/service/consultation_service.rs | 52 +++++++++++++++ 7 files changed, 188 insertions(+), 14 deletions(-) diff --git a/apps/miniprogram/src/pages/article/index.scss b/apps/miniprogram/src/pages/article/index.scss index ced095e..4f412f4 100644 --- a/apps/miniprogram/src/pages/article/index.scss +++ b/apps/miniprogram/src/pages/article/index.scss @@ -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; diff --git a/apps/miniprogram/src/pages/article/index.tsx b/apps/miniprogram/src/pages/article/index.tsx index 224d3e6..3cdc8b3 100644 --- a/apps/miniprogram/src/pages/article/index.tsx +++ b/apps/miniprogram/src/pages/article/index.tsx @@ -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([]); + const [activeCategory, setActiveCategory] = useState(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 ( + {/* 分类筛选 */} + {categories.length > 0 && ( + + handleCategoryChange(null)} + > + 全部 + + {categories.map((cat) => ( + handleCategoryChange(cat.id)} + > + {cat.name} + + ))} + + )} + {articles.map((a) => ( {a.summary} )} - {a.category && ( - {a.category} + {(a.category_name || a.category) && ( + {a.category_name || a.category} )} {a.published_at && ( diff --git a/apps/miniprogram/src/pages/doctor/index.tsx b/apps/miniprogram/src/pages/doctor/index.tsx index 796a978..f79462e 100644 --- a/apps/miniprogram/src/pages/doctor/index.tsx +++ b/apps/miniprogram/src/pages/doctor/index.tsx @@ -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(null); @@ -86,6 +92,24 @@ export default function DoctorHome() { + + 健康审核 + + {HEALTH_CARDS.map((card) => ( + handleCardClick(card)} + > + {card.icon} + {getValue(card.key)} + {card.label} + + ))} + + + 快捷操作 diff --git a/apps/miniprogram/src/services/article.ts b/apps/miniprogram/src/services/article.ts index 5667296..3da177b 100644 --- a/apps/miniprogram/src/services/article.ts +++ b/apps/miniprogram/src/services/article.ts @@ -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
(`/health/articles/${id}`); } + +export async function listCategories() { + return api.get('/health/article-categories'); +} diff --git a/apps/miniprogram/src/services/doctor.ts b/apps/miniprogram/src/services/doctor.ts index 5cc69d9..7191b82 100644 --- a/apps/miniprogram/src/services/doctor.ts +++ b/apps/miniprogram/src/services/doctor.ts @@ -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() { diff --git a/crates/erp-health/src/handler/consultation_handler.rs b/crates/erp-health/src/handler/consultation_handler.rs index d840d9a..06b3972 100644 --- a/crates/erp-health/src/handler/consultation_handler.rs +++ b/crates/erp-health/src/handler/consultation_handler.rs @@ -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))) } diff --git a/crates/erp-health/src/service/consultation_service.rs b/crates/erp-health/src/service/consultation_service.rs index ac5e465..62661f3 100644 --- a/crates/erp-health/src/service/consultation_service.rs +++ b/crates/erp-health/src/service/consultation_service.rs @@ -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(()) +}