refactor(mp): 迁移告警列表页 — 使用统一组件库 PageShell/ContentCard/StatusTag/LoadingCard/SearchSection/PaginationBar
This commit is contained in:
@@ -1,18 +1,16 @@
|
|||||||
@import '../../../styles/variables.scss';
|
@import '../../../styles/variables.scss';
|
||||||
@import '../../../styles/mixins.scss';
|
|
||||||
|
|
||||||
.alert-list-page {
|
// PageShell 已接管:min-height, background, padding
|
||||||
min-height: 100vh;
|
// SearchSection 已接管:筛选标签栏
|
||||||
background: $bg;
|
// ContentCard 已接管:alert-card 背景/圆角/阴影/触摸反馈
|
||||||
padding: 24px;
|
// StatusTag 已接管:告警严重度和状态标签样式
|
||||||
padding-bottom: 120px;
|
// PaginationBar 已接管:分页控件
|
||||||
}
|
|
||||||
|
|
||||||
.alert-list-header {
|
.alert-list-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-list-title {
|
.alert-list-title {
|
||||||
@@ -29,126 +27,47 @@
|
|||||||
.alert-cards {
|
.alert-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: var(--tk-gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-card {
|
// 告警卡片左侧彩色边框 — 业务特有,不归 ContentCard
|
||||||
background: $card;
|
.alert-card--critical {
|
||||||
border-radius: $r-lg;
|
border-left: 4px solid $dan;
|
||||||
padding: 24px;
|
}
|
||||||
box-shadow: $shadow-sm;
|
|
||||||
|
.alert-card--warning {
|
||||||
border-left: 4px solid $wrn;
|
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 {
|
.alert-card--info {
|
||||||
font-size: var(--tk-font-body);
|
border-left: 4px solid $tx3;
|
||||||
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 {
|
.alert-card--urgent {
|
||||||
font-size: var(--tk-font-body);
|
border-left: 4px solid $dan;
|
||||||
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 {
|
.alert-card__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
margin-bottom: 12px;
|
||||||
margin-top: 32px;
|
}
|
||||||
|
|
||||||
&__btn {
|
.alert-card__title {
|
||||||
font-size: var(--tk-font-h1);
|
font-size: var(--tk-font-body-lg);
|
||||||
color: $pri;
|
font-weight: 500;
|
||||||
padding: 12px 24px;
|
color: $tx;
|
||||||
|
margin-bottom: 8px;
|
||||||
&.disabled {
|
}
|
||||||
color: $tx3;
|
|
||||||
}
|
.alert-card__footer {
|
||||||
}
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
&__info {
|
align-items: center;
|
||||||
font-size: var(--tk-font-h2);
|
}
|
||||||
color: $tx2;
|
|
||||||
}
|
.alert-card__time {
|
||||||
|
font-size: var(--tk-font-body);
|
||||||
|
color: $tx3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,53 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { View, Text, ScrollView } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { usePageData } from '@/hooks/usePageData';
|
import { usePageData } from '@/hooks/usePageData';
|
||||||
import { listAlerts, type Alert } from '@/services/doctor/alerts';
|
import { listAlerts, type Alert } from '@/services/doctor/alerts';
|
||||||
import Loading from '@/components/Loading';
|
import PageShell from '@/components/ui/PageShell';
|
||||||
|
import ContentCard from '@/components/ui/ContentCard';
|
||||||
|
import StatusTag from '@/components/ui/StatusTag';
|
||||||
|
import LoadingCard from '@/components/ui/LoadingCard';
|
||||||
|
import PaginationBar from '@/components/patterns/PaginationBar';
|
||||||
|
import SearchSection from '@/components/patterns/SearchSection';
|
||||||
import ErrorState from '@/components/ErrorState';
|
import ErrorState from '@/components/ErrorState';
|
||||||
import EmptyState from '@/components/EmptyState';
|
import EmptyState from '@/components/EmptyState';
|
||||||
import SegmentTabs from '@/components/SegmentTabs';
|
|
||||||
import { useElderClass } from '../../../hooks/useElderClass';
|
import { useElderClass } from '../../../hooks/useElderClass';
|
||||||
import { safeNavigateTo } from '@/utils/navigate';
|
import { safeNavigateTo } from '@/utils/navigate';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
|
const SEVERITY_COLOR_MAP: Record<string, 'success' | 'warning' | 'error' | 'default'> = {
|
||||||
info: { label: '提示', className: 'alert-severity--info' },
|
info: 'default',
|
||||||
warning: { label: '警告', className: 'alert-severity--warning' },
|
warning: 'warning',
|
||||||
critical: { label: '严重', className: 'alert-severity--critical' },
|
critical: 'error',
|
||||||
urgent: { label: '紧急', className: 'alert-severity--urgent' },
|
urgent: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
const SEVERITY_LABEL: Record<string, string> = {
|
||||||
pending: { label: '待处理', className: 'alert-status--pending' },
|
info: '提示',
|
||||||
acknowledged: { label: '已确认', className: 'alert-status--acknowledged' },
|
warning: '警告',
|
||||||
resolved: { label: '已恢复', className: 'alert-status--resolved' },
|
critical: '严重',
|
||||||
dismissed: { label: '已忽略', className: 'alert-status--dismissed' },
|
urgent: '紧急',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_TABS = [
|
const STATUS_COLOR_MAP: Record<string, 'success' | 'warning' | 'info' | 'default'> = {
|
||||||
{ value: '', label: '全部' },
|
pending: 'warning',
|
||||||
{ value: 'pending', label: '待处理' },
|
acknowledged: 'info',
|
||||||
{ value: 'acknowledged', label: '已确认' },
|
resolved: 'success',
|
||||||
{ value: 'resolved', label: '已恢复' },
|
dismissed: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<string, string> = {
|
||||||
|
pending: '待处理',
|
||||||
|
acknowledged: '已确认',
|
||||||
|
resolved: '已恢复',
|
||||||
|
dismissed: '已忽略',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_FILTERS = [
|
||||||
|
{ key: '', label: '全部' },
|
||||||
|
{ key: 'pending', label: '待处理' },
|
||||||
|
{ key: 'acknowledged', label: '已确认' },
|
||||||
|
{ key: 'resolved', label: '已恢复' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AlertList() {
|
export default function AlertList() {
|
||||||
@@ -42,8 +60,6 @@ export default function AlertList() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const mountedRef = useRef(false);
|
const mountedRef = useRef(false);
|
||||||
|
|
||||||
const totalPages = useMemo(() => Math.ceil(total / 20), [total]);
|
|
||||||
|
|
||||||
const loadAlerts = useCallback(async () => {
|
const loadAlerts = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
@@ -65,7 +81,6 @@ export default function AlertList() {
|
|||||||
|
|
||||||
const { trigger } = usePageData(loadAlerts);
|
const { trigger } = usePageData(loadAlerts);
|
||||||
|
|
||||||
// tab/page 变化时重新加载(跳过首次 mount,由 usePageData 的 useDidShow 处理)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
trigger();
|
trigger();
|
||||||
@@ -94,89 +109,79 @@ export default function AlertList() {
|
|||||||
return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && alerts.length === 0) return <Loading />;
|
if (loading && alerts.length === 0) return <LoadingCard count={3} />;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ScrollView scrollY className={`alert-list-page ${modeClass}`}>
|
<PageShell safeBottom className={modeClass}>
|
||||||
<View className='alert-list-header'>
|
<SearchSection
|
||||||
<Text className='alert-list-title'>告警列表</Text>
|
value=""
|
||||||
</View>
|
onChange={() => {}}
|
||||||
<View className='alert-tabs'>
|
filters={STATUS_FILTERS}
|
||||||
{STATUS_TABS.map((tab) => (
|
activeFilter={activeTab}
|
||||||
<Text
|
onFilterChange={handleTabChange}
|
||||||
key={tab.value}
|
/>
|
||||||
className={`alert-tab ${activeTab === tab.value ? 'alert-tab--active' : ''}`}
|
|
||||||
onClick={() => handleTabChange(tab.value)}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
<ErrorState onRetry={loadAlerts} />
|
<ErrorState onRetry={loadAlerts} />
|
||||||
</ScrollView>
|
</PageShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView scrollY className={`alert-list-page ${modeClass}`}>
|
<PageShell safeBottom className={modeClass}>
|
||||||
<View className='alert-list-header'>
|
<View className="alert-list-header">
|
||||||
<Text className='alert-list-title'>告警列表</Text>
|
<Text className="alert-list-title">告警列表</Text>
|
||||||
<Text className='alert-list-count'>共 {total} 条</Text>
|
<Text className="alert-list-count">共 {total} 条</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<SegmentTabs tabs={STATUS_TABS.map(t => ({ key: t.value, label: t.label }))} activeKey={activeTab} onChange={handleTabChange} variant="pill" />
|
<SearchSection
|
||||||
|
value=""
|
||||||
|
onChange={() => {}}
|
||||||
|
filters={STATUS_FILTERS}
|
||||||
|
activeFilter={activeTab}
|
||||||
|
onFilterChange={handleTabChange}
|
||||||
|
/>
|
||||||
|
|
||||||
{alerts.length === 0 ? (
|
{alerts.length === 0 ? (
|
||||||
<EmptyState description='暂无告警' />
|
<EmptyState text="暂无告警" />
|
||||||
) : (
|
) : (
|
||||||
<View className='alert-cards'>
|
<View className="alert-cards">
|
||||||
{alerts.map((alert) => {
|
{alerts.map((alert) => (
|
||||||
const severity = SEVERITY_MAP[alert.severity] ?? SEVERITY_MAP.info;
|
<ContentCard
|
||||||
const status = STATUS_MAP[alert.status] ?? STATUS_MAP.pending;
|
key={alert.id}
|
||||||
return (
|
onPress={() => handleAlertClick(alert)}
|
||||||
<View
|
className={`alert-card--${alert.severity || 'info'}`}
|
||||||
key={alert.id}
|
>
|
||||||
className='alert-card'
|
<View className="alert-card__header">
|
||||||
onClick={() => handleAlertClick(alert)}
|
<StatusTag
|
||||||
>
|
status={alert.severity}
|
||||||
<View className='alert-card__header'>
|
colorMap={SEVERITY_COLOR_MAP}
|
||||||
<Text className={`alert-severity ${severity.className}`}>
|
size="sm"
|
||||||
{severity.label}
|
>
|
||||||
</Text>
|
{SEVERITY_LABEL[alert.severity] || '提示'}
|
||||||
<Text className={`alert-status ${status.className}`}>
|
</StatusTag>
|
||||||
{status.label}
|
<StatusTag
|
||||||
</Text>
|
status={alert.status}
|
||||||
</View>
|
colorMap={STATUS_COLOR_MAP}
|
||||||
<Text className='alert-card__title'>{alert.title}</Text>
|
size="sm"
|
||||||
<View className='alert-card__footer'>
|
>
|
||||||
<Text className='alert-card__time'>{formatTime(alert.created_at)}</Text>
|
{STATUS_LABEL[alert.status] || '未知'}
|
||||||
</View>
|
</StatusTag>
|
||||||
</View>
|
</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>
|
||||||
|
</ContentCard>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{total > 20 && (
|
<PaginationBar
|
||||||
<View className='alert-pagination'>
|
current={page}
|
||||||
<Text
|
total={total}
|
||||||
className={`alert-pagination__btn ${page <= 1 ? 'disabled' : ''}`}
|
pageSize={20}
|
||||||
onClick={() => page > 1 && setPage(page - 1)}
|
onChange={setPage}
|
||||||
>
|
/>
|
||||||
上一页
|
</PageShell>
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user