feat(mp): 患者端健康告警页面 + 首页入口
P1-8: 小程序患者告警推送 - 新增 alert service:listPatientAlerts 按患者 ID 查询告警 - 新增 pkg-health/alerts 告警列表页:严重程度标签 + 状态过滤 + 分页 - 首页快捷服务新增"健康告警"入口 - app.config.ts 注册 alerts/index 页面路由
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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' },
|
||||||
];
|
];
|
||||||
|
|||||||
134
apps/miniprogram/src/pages/pkg-health/alerts/index.scss
Normal file
134
apps/miniprogram/src/pages/pkg-health/alerts/index.scss
Normal 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;
|
||||||
|
}
|
||||||
133
apps/miniprogram/src/pages/pkg-health/alerts/index.tsx
Normal file
133
apps/miniprogram/src/pages/pkg-health/alerts/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
apps/miniprogram/src/services/alert.ts
Normal file
21
apps/miniprogram/src/services/alert.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user