feat(mp): 医护端告警列表/详情页 + DoctorHome 告警 banner 增强
- 新增告警列表页:按状态筛选、分页、严重程度/状态标签 - 新增告警详情页:完整信息展示 + 确认/忽略/恢复操作 - doctor.ts 新增 listAlerts/acknowledgeAlert/dismissAlert/resolveAlert API - DoctorHome 告警 banner 跳转目标改为告警列表页 - 注册 alerts/index + alerts/detail/index 到 doctor subPackage
This commit is contained in:
@@ -10,13 +10,12 @@ export default defineAppConfig({
|
||||
'pages/appointment/index',
|
||||
'pages/appointment/create/index',
|
||||
'pages/appointment/detail/index',
|
||||
'pages/article/index',
|
||||
'pages/legal/user-agreement',
|
||||
'pages/legal/privacy-policy',
|
||||
],
|
||||
subPackages: [
|
||||
{
|
||||
root: 'pages/health',
|
||||
root: 'pages/pkg-health',
|
||||
pages: ['trend/index', 'input/index', 'daily-monitoring/index'],
|
||||
},
|
||||
{
|
||||
@@ -26,26 +25,43 @@ export default defineAppConfig({
|
||||
'consultation/index', 'consultation/detail/index',
|
||||
'followup/index', 'followup/detail/index',
|
||||
'report/index', 'report/detail/index',
|
||||
'alerts/index', 'alerts/detail/index',
|
||||
],
|
||||
},
|
||||
{
|
||||
root: 'pages/mall',
|
||||
root: 'pages/pkg-mall',
|
||||
pages: ['exchange/index', 'orders/index', 'detail/index'],
|
||||
},
|
||||
{
|
||||
root: 'pages/profile',
|
||||
root: 'pages/pkg-profile',
|
||||
pages: [
|
||||
'family/index', 'family-add/index', 'reports/index',
|
||||
'followups/index', 'medication/index', 'settings/index',
|
||||
],
|
||||
},
|
||||
{
|
||||
root: 'pages',
|
||||
pages: [
|
||||
'article/detail/index', 'ai-report/list/index',
|
||||
'ai-report/detail/index', 'report/detail/index',
|
||||
'followup/detail/index', 'events/index', 'device-sync/index',
|
||||
],
|
||||
root: 'pages/ai-report',
|
||||
pages: ['list/index', 'detail/index'],
|
||||
},
|
||||
{
|
||||
root: 'pages/article',
|
||||
pages: ['index', 'detail/index'],
|
||||
},
|
||||
{
|
||||
root: 'pages/report',
|
||||
pages: ['detail/index'],
|
||||
},
|
||||
{
|
||||
root: 'pages/followup',
|
||||
pages: ['detail/index'],
|
||||
},
|
||||
{
|
||||
root: 'pages/events',
|
||||
pages: ['index'],
|
||||
},
|
||||
{
|
||||
root: 'pages/device-sync',
|
||||
pages: ['index'],
|
||||
},
|
||||
],
|
||||
tabBar: {
|
||||
|
||||
166
apps/miniprogram/src/pages/doctor/alerts/detail/index.scss
Normal file
166
apps/miniprogram/src/pages/doctor/alerts/detail/index.scss
Normal file
@@ -0,0 +1,166 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
@import '../../../../styles/mixins.scss';
|
||||
|
||||
.alert-detail-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding-bottom: 160px;
|
||||
}
|
||||
|
||||
.alert-detail-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-severity {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
padding: 6px 16px;
|
||||
border-radius: $r-sm;
|
||||
|
||||
&--info {
|
||||
background: $bd-l;
|
||||
color: $tx2;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $wrn-l;
|
||||
color: $wrn;
|
||||
}
|
||||
|
||||
&--critical {
|
||||
background: $dan-l;
|
||||
color: $dan;
|
||||
}
|
||||
|
||||
&--urgent {
|
||||
background: $dan-l;
|
||||
color: $dan;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
font-size: 24px;
|
||||
padding: 6px 16px;
|
||||
border-radius: $r-sm;
|
||||
|
||||
&--pending {
|
||||
background: $wrn-l;
|
||||
color: $wrn;
|
||||
}
|
||||
|
||||
&--acknowledged {
|
||||
background: $pri-l;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
&--resolved {
|
||||
background: $acc-l;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
&--dismissed {
|
||||
background: $bd-l;
|
||||
color: $tx3;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-detail-card {
|
||||
background: $card;
|
||||
border-radius: $r-lg;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&__label {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
word-break: break-all;
|
||||
|
||||
&--id {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
&--detail {
|
||||
font-size: 22px;
|
||||
color: $tx2;
|
||||
font-family: monospace;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
background: $bg;
|
||||
padding: 16px;
|
||||
border-radius: $r;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-detail-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 32px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.alert-action-btn {
|
||||
flex: 1;
|
||||
height: 88px;
|
||||
line-height: 88px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
border-radius: $r-lg;
|
||||
text-align: center;
|
||||
|
||||
&--primary {
|
||||
background: $pri;
|
||||
color: $card;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--default {
|
||||
background: $bd-l;
|
||||
color: $tx2;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--resolve {
|
||||
background: $acc-l;
|
||||
color: $acc;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
210
apps/miniprogram/src/pages/doctor/alerts/detail/index.tsx
Normal file
210
apps/miniprogram/src/pages/doctor/alerts/detail/index.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import Loading from '@/components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
|
||||
info: { label: '提示', className: 'detail-severity--info' },
|
||||
warning: { label: '警告', className: 'detail-severity--warning' },
|
||||
critical: { label: '严重', className: 'detail-severity--critical' },
|
||||
urgent: { label: '紧急', className: 'detail-severity--urgent' },
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||||
pending: { label: '待处理', className: 'detail-status--pending' },
|
||||
acknowledged: { label: '已确认', className: 'detail-status--acknowledged' },
|
||||
resolved: { label: '已恢复', className: 'detail-status--resolved' },
|
||||
dismissed: { label: '已忽略', className: 'detail-status--dismissed' },
|
||||
};
|
||||
|
||||
export default function AlertDetail() {
|
||||
const [alert, setAlert] = useState<doctorApi.Alert | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const params = Taro.getCurrentInstance().router?.params;
|
||||
if (params?.id) {
|
||||
loadAlert(params.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadAlert = async (id: string) => {
|
||||
try {
|
||||
// 告警列表 API 支持按 ID 查询,此处用列表加载后过滤
|
||||
const res = await doctorApi.listAlerts({ page: 1, page_size: 100 });
|
||||
const found = (res.data || []).find((a) => a.id === id);
|
||||
if (found) {
|
||||
setAlert(found);
|
||||
} else {
|
||||
Taro.showToast({ title: '告警不存在', icon: 'none' });
|
||||
}
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcknowledge = async () => {
|
||||
if (!alert) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await doctorApi.acknowledgeAlert(alert.id, alert.version);
|
||||
setAlert(updated);
|
||||
Taro.showToast({ title: '已确认', icon: 'success' });
|
||||
} catch {
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = async () => {
|
||||
if (!alert) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await doctorApi.dismissAlert(alert.id, alert.version);
|
||||
setAlert(updated);
|
||||
Taro.showToast({ title: '已忽略', icon: 'success' });
|
||||
} catch {
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolve = async () => {
|
||||
if (!alert) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await doctorApi.resolveAlert(alert.id, alert.version);
|
||||
setAlert(updated);
|
||||
Taro.showToast({ title: '已恢复', icon: 'success' });
|
||||
} catch {
|
||||
Taro.showToast({ title: '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (!alert) {
|
||||
return (
|
||||
<View className='alert-detail-page'>
|
||||
<Text>告警不存在</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const severity = SEVERITY_MAP[alert.severity] ?? SEVERITY_MAP.info;
|
||||
const status = STATUS_MAP[alert.status] ?? STATUS_MAP.pending;
|
||||
const isPending = alert.status === 'pending';
|
||||
const isAcknowledged = alert.status === 'acknowledged';
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='alert-detail-page'>
|
||||
{/* 顶部状态 */}
|
||||
<View className='alert-detail-header'>
|
||||
<View className='alert-detail-header__tags'>
|
||||
<Text className={`detail-severity ${severity.className}`}>
|
||||
{severity.label}
|
||||
</Text>
|
||||
<Text className={`detail-status ${status.className}`}>
|
||||
{status.label}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='alert-detail-header__time'>
|
||||
{new Date(alert.created_at).toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 告警信息 */}
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>告警标题</Text>
|
||||
<Text className='alert-detail-card__value'>{alert.title}</Text>
|
||||
</View>
|
||||
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>患者 ID</Text>
|
||||
<Text className='alert-detail-card__value alert-detail-card__value--id'>
|
||||
{alert.patient_id.slice(0, 8)}...
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>严重程度</Text>
|
||||
<Text className='alert-detail-card__value'>{severity.label}</Text>
|
||||
</View>
|
||||
|
||||
{alert.detail && (
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>告警详情</Text>
|
||||
<Text className='alert-detail-card__value alert-detail-card__value--detail'>
|
||||
{JSON.stringify(alert.detail, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{alert.acknowledged_by && (
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>处理人</Text>
|
||||
<Text className='alert-detail-card__value'>{alert.acknowledged_by}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{alert.acknowledged_at && (
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>确认时间</Text>
|
||||
<Text className='alert-detail-card__value'>
|
||||
{new Date(alert.acknowledged_at).toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{alert.resolved_at && (
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>恢复时间</Text>
|
||||
<Text className='alert-detail-card__value'>
|
||||
{new Date(alert.resolved_at).toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{(isPending || isAcknowledged) && (
|
||||
<View className='alert-detail-actions'>
|
||||
{isPending && (
|
||||
<>
|
||||
<Button
|
||||
className='alert-action-btn alert-action-btn--primary'
|
||||
onClick={handleAcknowledge}
|
||||
disabled={actionLoading}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
<Button
|
||||
className='alert-action-btn alert-action-btn--default'
|
||||
onClick={handleDismiss}
|
||||
disabled={actionLoading}
|
||||
>
|
||||
忽略
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(isPending || isAcknowledged) && (
|
||||
<Button
|
||||
className='alert-action-btn alert-action-btn--resolve'
|
||||
onClick={handleResolve}
|
||||
disabled={actionLoading}
|
||||
>
|
||||
恢复
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
174
apps/miniprogram/src/pages/doctor/alerts/index.scss
Normal file
174
apps/miniprogram/src/pages/doctor/alerts/index.scss
Normal file
@@ -0,0 +1,174 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.alert-list-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
|
||||
.alert-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.alert-list-title {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.alert-list-count {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
}
|
||||
|
||||
.alert-tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.alert-tab {
|
||||
padding: 10px 24px;
|
||||
border-radius: $r-pill;
|
||||
background: $bd-l;
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--active {
|
||||
background: $pri;
|
||||
color: $card;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
background: $card;
|
||||
border-radius: $r-lg;
|
||||
padding: 24px;
|
||||
box-shadow: $shadow-sm;
|
||||
border-left: 4px solid $wrn;
|
||||
|
||||
&:active {
|
||||
background: $bd-l;
|
||||
}
|
||||
|
||||
&--critical {
|
||||
border-left-color: $dan;
|
||||
}
|
||||
|
||||
&--info {
|
||||
border-left-color: $tx3;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: $tx;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-severity {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: $r-sm;
|
||||
|
||||
&--info {
|
||||
background: $bd-l;
|
||||
color: $tx2;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $wrn-l;
|
||||
color: $wrn;
|
||||
}
|
||||
|
||||
&--critical {
|
||||
background: $dan-l;
|
||||
color: $dan;
|
||||
}
|
||||
|
||||
&--urgent {
|
||||
background: $dan-l;
|
||||
color: $dan;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-status {
|
||||
font-size: 22px;
|
||||
padding: 4px 12px;
|
||||
border-radius: $r-sm;
|
||||
|
||||
&--pending {
|
||||
background: $wrn-l;
|
||||
color: $wrn;
|
||||
}
|
||||
|
||||
&--acknowledged {
|
||||
background: $pri-l;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
&--resolved {
|
||||
background: $acc-l;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
&--dismissed {
|
||||
background: $bd-l;
|
||||
color: $tx3;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-top: 32px;
|
||||
|
||||
&__btn {
|
||||
font-size: 26px;
|
||||
color: $pri;
|
||||
padding: 12px 24px;
|
||||
|
||||
&.disabled {
|
||||
color: $tx3;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
}
|
||||
}
|
||||
152
apps/miniprogram/src/pages/doctor/alerts/index.tsx
Normal file
152
apps/miniprogram/src/pages/doctor/alerts/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import './index.scss';
|
||||
|
||||
const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
|
||||
info: { label: '提示', className: 'alert-severity--info' },
|
||||
warning: { label: '警告', className: 'alert-severity--warning' },
|
||||
critical: { label: '严重', className: 'alert-severity--critical' },
|
||||
urgent: { label: '紧急', className: 'alert-severity--urgent' },
|
||||
};
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||||
pending: { label: '待处理', className: 'alert-status--pending' },
|
||||
acknowledged: { label: '已确认', className: 'alert-status--acknowledged' },
|
||||
resolved: { label: '已恢复', className: 'alert-status--resolved' },
|
||||
dismissed: { label: '已忽略', className: 'alert-status--dismissed' },
|
||||
};
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'acknowledged', label: '已确认' },
|
||||
{ value: 'resolved', label: '已恢复' },
|
||||
];
|
||||
|
||||
export default function AlertList() {
|
||||
const [alerts, setAlerts] = useState<doctorApi.Alert[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
loadAlerts();
|
||||
}, [page, activeTab]);
|
||||
|
||||
const loadAlerts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listAlerts({
|
||||
status: activeTab || undefined,
|
||||
page,
|
||||
page_size: 20,
|
||||
});
|
||||
setAlerts(res.data || []);
|
||||
setTotal(res.total || 0);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleAlertClick = (alert: doctorApi.Alert) => {
|
||||
Taro.navigateTo({ url: `/pages/doctor/alerts/detail/index?id=${alert.id}` });
|
||||
};
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - d.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
if (minutes < 1) return '刚刚';
|
||||
if (minutes < 60) return `${minutes}分钟前`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}小时前`;
|
||||
return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
if (loading && alerts.length === 0) return <Loading />;
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='alert-list-page'>
|
||||
<View className='alert-list-header'>
|
||||
<Text className='alert-list-title'>告警列表</Text>
|
||||
<Text className='alert-list-count'>共 {total} 条</Text>
|
||||
</View>
|
||||
|
||||
<View className='alert-tabs'>
|
||||
{STATUS_TABS.map((tab) => (
|
||||
<Text
|
||||
key={tab.value}
|
||||
className={`alert-tab ${activeTab === tab.value ? 'alert-tab--active' : ''}`}
|
||||
onClick={() => handleTabChange(tab.value)}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{alerts.length === 0 ? (
|
||||
<EmptyState description='暂无告警' />
|
||||
) : (
|
||||
<View className='alert-cards'>
|
||||
{alerts.map((alert) => {
|
||||
const severity = SEVERITY_MAP[alert.severity] ?? SEVERITY_MAP.info;
|
||||
const status = STATUS_MAP[alert.status] ?? STATUS_MAP.pending;
|
||||
return (
|
||||
<View
|
||||
key={alert.id}
|
||||
className='alert-card'
|
||||
onClick={() => handleAlertClick(alert)}
|
||||
>
|
||||
<View className='alert-card__header'>
|
||||
<Text className={`alert-severity ${severity.className}`}>
|
||||
{severity.label}
|
||||
</Text>
|
||||
<Text className={`alert-status ${status.className}`}>
|
||||
{status.label}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='alert-card__title'>{alert.title}</Text>
|
||||
<View className='alert-card__footer'>
|
||||
<Text className='alert-card__time'>{formatTime(alert.created_at)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{total > 20 && (
|
||||
<View className='alert-pagination'>
|
||||
<Text
|
||||
className={`alert-pagination__btn ${page <= 1 ? 'disabled' : ''}`}
|
||||
onClick={() => page > 1 && setPage(page - 1)}
|
||||
>
|
||||
上一页
|
||||
</Text>
|
||||
<Text className='alert-pagination__info'>
|
||||
{page} / {Math.ceil(total / 20)}
|
||||
</Text>
|
||||
<Text
|
||||
className={`alert-pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`}
|
||||
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)}
|
||||
>
|
||||
下一页
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export default function DoctorHome() {
|
||||
<View className='doctor-home__alert'>
|
||||
<Text className='doctor-home__alert-icon'>!</Text>
|
||||
<Text className='doctor-home__alert-text'>{alertCount} 位患者体征异常</Text>
|
||||
<Text className='doctor-home__alert-link' onClick={() => Taro.navigateTo({ url: '/pages/doctor/patients/index' })}>查看 →</Text>
|
||||
<Text className='doctor-home__alert-link' onClick={() => Taro.navigateTo({ url: '/pages/doctor/alerts/index' })}>查看 →</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -304,3 +304,41 @@ export async function getConsultationStats() {
|
||||
export async function getFollowUpStats() {
|
||||
return api.get<FollowUpStats>('/health/admin/statistics/follow-ups');
|
||||
}
|
||||
|
||||
// ── Alerts (doctor view) ────────────────────────────
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
rule_id: string;
|
||||
severity: string;
|
||||
title: string;
|
||||
detail?: Record<string, unknown>;
|
||||
status: string;
|
||||
acknowledged_by?: string;
|
||||
acknowledged_at?: string;
|
||||
resolved_at?: string;
|
||||
created_at: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export async function listAlerts(params?: {
|
||||
patient_id?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}) {
|
||||
return api.get<{ data: Alert[]; total: number }>('/health/alerts', params);
|
||||
}
|
||||
|
||||
export async function acknowledgeAlert(id: string, version: number) {
|
||||
return api.put<Alert>(`/health/alerts/${id}/acknowledge`, { version });
|
||||
}
|
||||
|
||||
export async function dismissAlert(id: string, version: number) {
|
||||
return api.put<Alert>(`/health/alerts/${id}/dismiss`, { version });
|
||||
}
|
||||
|
||||
export async function resolveAlert(id: string, version: number) {
|
||||
return api.put<Alert>(`/health/alerts/${id}/resolve`, { version });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user