refactor(mp): 分包策略优化 — 合并单页分包 + doctor 拆包 + consultation 移出主包

- 合并 4 个单页分包:report→pkg-profile/reports, followup→pkg-profile/followups,
  events→pkg-profile/events, device-sync→pkg-health
- consultation/detail 移出主包到 pkg-consultation 分包(减少主包体积)
- doctor 18 页拆分为 pkg-doctor-core(8页) + pkg-doctor-clinical(10页)
- 全部导航路径和 import 路径同步更新
- 分包 10→8 个,主包页面 13→12
This commit is contained in:
iven
2026-05-15 07:53:00 +08:00
parent 5baa518516
commit 4c38fcd89d
58 changed files with 71 additions and 78 deletions

View 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: var(--tk-font-h2);
color: $tx3;
}
}
.detail-severity {
font-size: var(--tk-font-h2);
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: var(--tk-font-h2);
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: var(--tk-font-h2);
color: $tx2;
margin-bottom: 8px;
}
&__value {
font-size: var(--tk-font-body-lg);
color: $tx;
word-break: break-all;
&--id {
font-size: var(--tk-font-h2);
color: $tx3;
font-family: monospace;
}
&--detail {
font-size: var(--tk-font-body);
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: var(--tk-font-body-lg);
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;
}
}

View File

@@ -0,0 +1,208 @@
import { useState, useCallback } from 'react';
import { View, Text, ScrollView, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import {
getAlert, acknowledgeAlert, dismissAlert, resolveAlert,
type Alert,
} from '@/services/doctor/alerts';
import Loading from '@/components/Loading';
import { useElderClass } from '../../../../hooks/useElderClass';
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 modeClass = useElderClass();
const [alert, setAlert] = useState<Alert | null>(null);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const alertId = Taro.getCurrentInstance().router?.params?.id || '';
const loadAlert = useCallback(async () => {
if (!alertId) return;
try {
const data = await getAlert(alertId);
setAlert(data);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
}, [alertId]);
usePageData(loadAlert, { throttleMs: 60000, enablePullDown: false, enabled: !!alertId });
const handleAcknowledge = async () => {
if (!alert) return;
setActionLoading(true);
try {
const updated = await 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 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 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 ${modeClass}`}>
<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 ${modeClass}`}>
{/* 顶部状态 */}
<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 ? `${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>
);
}

View 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: var(--tk-font-num-lg);
font-weight: 600;
color: $tx;
}
.alert-list-count {
font-size: var(--tk-font-h2);
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: var(--tk-font-h2);
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: var(--tk-font-body-lg);
font-weight: 500;
color: $tx;
margin-bottom: 8px;
}
&__footer {
display: flex;
justify-content: space-between;
align-items: center;
}
&__time {
font-size: var(--tk-font-body);
color: $tx3;
}
}
.alert-severity {
font-size: var(--tk-font-body);
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: var(--tk-font-body);
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: var(--tk-font-h1);
color: $pri;
padding: 12px 24px;
&.disabled {
color: $tx3;
}
}
&__info {
font-size: var(--tk-font-h2);
color: $tx2;
}
}

View File

@@ -0,0 +1,165 @@
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { listAlerts, type Alert } from '@/services/doctor/alerts';
import Loading from '@/components/Loading';
import EmptyState from '@/components/EmptyState';
import { useElderClass } from '../../../hooks/useElderClass';
import { safeNavigateTo } from '@/utils/navigate';
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 modeClass = useElderClass();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('');
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const mountedRef = useRef(false);
const totalPages = useMemo(() => Math.ceil(total / 20), [total]);
const loadAlerts = useCallback(async () => {
setLoading(true);
try {
const res = await listAlerts({
status: activeTab || undefined,
page,
page_size: 20,
});
setAlerts(res.data || []);
setTotal(res.total || 0);
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
}, [activeTab, page]);
const { trigger } = usePageData(loadAlerts);
// tab/page 变化时重新加载(跳过首次 mount由 usePageData 的 useDidShow 处理)
useEffect(() => {
if (mountedRef.current) {
trigger();
}
mountedRef.current = true;
}, [page, activeTab, trigger]);
const handleTabChange = (value: string) => {
setActiveTab(value);
setPage(1);
};
const handleAlertClick = (alert: Alert) => {
safeNavigateTo(`/pages/pkg-doctor-clinical/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 ${modeClass}`}>
<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} / {totalPages}
</Text>
<Text
className={`alert-pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < totalPages && setPage(page + 1)}
>
</Text>
</View>
)}
</ScrollView>
);
}