feat(mp): 患者端健康告警页面 + 首页入口
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

P1-8: 小程序患者告警推送
  - 新增 alert service:listPatientAlerts 按患者 ID 查询告警
  - 新增 pkg-health/alerts 告警列表页:严重程度标签 + 状态过滤 + 分页
  - 首页快捷服务新增"健康告警"入口
  - app.config.ts 注册 alerts/index 页面路由
This commit is contained in:
iven
2026-04-30 07:23:05 +08:00
parent 26a9781d4f
commit 43769dae5a
5 changed files with 290 additions and 1 deletions

View File

@@ -16,7 +16,7 @@ export default defineAppConfig({
subPackages: [ subPackages: [
{ {
root: 'pages/pkg-health', root: 'pages/pkg-health',
pages: ['trend/index', 'input/index', 'daily-monitoring/index'], pages: ['trend/index', 'input/index', 'daily-monitoring/index', 'alerts/index'],
}, },
{ {
root: 'pages/doctor', root: 'pages/doctor',

View File

@@ -14,6 +14,7 @@ const QUICK_SERVICES = [
{ label: '预约挂号', char: '约', path: '/pages/appointment/create/index' }, { label: '预约挂号', char: '约', path: '/pages/appointment/create/index' },
{ label: '健康录入', char: '录', path: '/pages/pkg-health/input/index' }, { label: '健康录入', char: '录', path: '/pages/pkg-health/input/index' },
{ label: '健康趋势', char: '势', path: '/pages/pkg-health/trend/index' }, { label: '健康趋势', char: '势', path: '/pages/pkg-health/trend/index' },
{ label: '健康告警', char: '警', path: '/pages/pkg-health/alerts/index' },
{ label: '资讯文章', char: '文', path: '/pages/article/index' }, { label: '资讯文章', char: '文', path: '/pages/article/index' },
{ label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' }, { label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' },
]; ];

View File

@@ -0,0 +1,134 @@
.alerts-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: env(safe-area-inset-bottom);
}
.alerts-tabs {
display: flex;
background: #fff;
padding: 20px 16px;
gap: 16px;
border-bottom: 1px solid #eee;
}
.alerts-tab {
padding: 8px 20px;
border-radius: 20px;
background: #f0f0f0;
}
.alerts-tab.active {
background: #C4623A;
}
.alerts-tab-text {
font-size: 26px;
color: #666;
}
.alerts-tab-text.active {
color: #fff;
}
.alerts-list {
padding: 16px;
}
.alert-card {
background: #fff;
border-radius: 16px;
padding: 24px;
margin-bottom: 16px;
}
.alert-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.alert-badge {
padding: 4px 16px;
border-radius: 8px;
}
.alert-badge.sev-info {
background: #e6f7ff;
}
.alert-badge.sev-warning {
background: #fff7e6;
}
.alert-badge.sev-critical {
background: #fff1f0;
}
.alert-badge.sev-urgent {
background: #ff4d4f;
}
.alert-badge-text {
font-size: 22px;
font-weight: 600;
}
.alert-badge.sev-info .alert-badge-text {
color: #1890ff;
}
.alert-badge.sev-warning .alert-badge-text {
color: #fa8c16;
}
.alert-badge.sev-critical .alert-badge-text {
color: #f5222d;
}
.alert-badge.sev-urgent .alert-badge-text {
color: #fff;
}
.alert-time {
font-size: 24px;
color: #999;
}
.alert-title {
font-size: 28px;
color: #333;
line-height: 1.5;
}
.alerts-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120px 0;
}
.alerts-empty-text {
font-size: 30px;
color: #999;
margin-bottom: 16px;
}
.alerts-empty-hint {
font-size: 26px;
color: #bbb;
}
.alerts-empty-action {
margin-top: 24px;
padding: 16px 48px;
background: #C4623A;
border-radius: 32px;
}
.alerts-empty-action-text {
color: #fff;
font-size: 28px;
}

View File

@@ -0,0 +1,133 @@
import React, { useState, useCallback, useRef } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
import { listPatientAlerts, type Alert } from '@/services/alert';
import { useAuthStore } from '@/stores/auth';
import Loading from '@/components/Loading';
import './index.scss';
const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
info: { label: '提示', className: 'sev-info' },
warning: { label: '警告', className: 'sev-warning' },
critical: { label: '严重', className: 'sev-critical' },
urgent: { label: '紧急', className: 'sev-urgent' },
};
const STATUS_TABS = [
{ key: '', label: '全部' },
{ key: 'pending', label: '待处理' },
{ key: 'acknowledged', label: '已确认' },
{ key: 'resolved', label: '已恢复' },
];
export default function PatientAlerts() {
const { currentPatient } = useAuthStore();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
const loadingRef = useRef(false);
const fetchAlerts = useCallback(
async (pageNum: number, s: string, isRefresh = false) => {
if (!currentPatient || loadingRef.current) return;
loadingRef.current = true;
setLoading(true);
try {
const res = await listPatientAlerts(currentPatient.id, {
page: pageNum,
page_size: 20,
status: s || undefined,
});
const list = res.data || [];
if (isRefresh) {
setAlerts(list);
} else {
setAlerts((prev) => [...prev, ...list]);
}
setTotal(res.total);
setPage(pageNum);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
loadingRef.current = false;
setLoading(false);
}
},
[currentPatient],
);
useDidShow(() => {
Taro.setNavigationBarTitle({ title: '健康告警' });
fetchAlerts(1, status, true);
});
usePullDownRefresh(() => {
fetchAlerts(1, status, true).finally(() => Taro.stopPullDownRefresh());
});
const handleTabChange = (key: string) => {
setStatus(key);
fetchAlerts(1, key, true);
};
if (!currentPatient) {
return (
<View className='alerts-page'>
<View className='alerts-empty'>
<Text className='alerts-empty-text'></Text>
<View className='alerts-empty-action' onClick={() => Taro.navigateTo({ url: '/pages/pkg-profile/family-add/index' })}>
<Text className='alerts-empty-action-text'></Text>
</View>
</View>
</View>
);
}
return (
<View className='alerts-page'>
<View className='alerts-tabs'>
{STATUS_TABS.map((tab) => (
<View
key={tab.key}
className={`alerts-tab ${status === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text className={`alerts-tab-text ${status === tab.key ? 'active' : ''}`}>{tab.label}</Text>
</View>
))}
</View>
{alerts.length === 0 && !loading ? (
<View className='alerts-empty'>
<Text className='alerts-empty-text'></Text>
<Text className='alerts-empty-hint'></Text>
</View>
) : (
<View className='alerts-list'>
{alerts.map((item) => {
const sev = SEVERITY_MAP[item.severity] || SEVERITY_MAP.warning;
return (
<View className='alert-card' key={item.id}>
<View className='alert-header'>
<View className={`alert-badge ${sev.className}`}>
<Text className='alert-badge-text'>{sev.label}</Text>
</View>
<Text className='alert-time'>
{new Date(item.created_at).toLocaleDateString()}
</Text>
</View>
<Text className='alert-title'>{item.title}</Text>
</View>
);
})}
{loading && <Loading />}
{!loading && alerts.length >= total && total > 0 && (
<Loading text='没有更多了' />
)}
</View>
)}
</View>
);
}

View File

@@ -0,0 +1,21 @@
import { api } from './request';
export interface Alert {
id: string;
severity: string;
title: string;
status: string;
detail?: Record<string, unknown>;
created_at: string;
acknowledged_at?: string;
resolved_at?: string;
}
export async function listPatientAlerts(patientId: string, params?: { status?: string; page?: number; page_size?: number }) {
return api.get<{ data: Alert[]; total: number }>('/health/alerts', {
patient_id: patientId,
page: params?.page ?? 1,
page_size: params?.page_size ?? 20,
status: params?.status,
});
}