Files
hms/apps/web/src/pages/health/StatisticsDashboard/OperatorDashboard.tsx
iven a78673ef41 fix(web): PP-09 工作台死链修复 + value={0} 接入真实数据
StatisticsDashboard 在 feat 重构时引入死链 navigate(routeConfig 不存在的
路由)+ value={0} 占位假数据。影响 100% 角色首屏。

死链修复(对照 routeConfig 实际路由):
- /health/follow-ups → /health/follow-up-tasks
- /health/vital-signs → /health/daily-monitoring
- /health/lab-reports → /health/patients(无化验单独立页)
- /health/points → /health/points-rules
- /health/articles/edit/:id → /health/articles/:id/edit(参数位置)

value={0}(OperatorDashboard 线下活动): 接入真实 offlineEventCount
(useStatsData 调 listOfflineEvents 取 total),消除假数据。

含 feat 进行中的 StatisticsDashboard 重构(卡片化 navigate + useStatsData
统一数据层)+ 清理未使用 import + fetchTopArticles effect 标注。待办:
AdminDashboard「咨询待回复」仍有 value={0} 占位。
2026-06-26 11:19:07 +08:00

131 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Row, Col, Card, Statistic, List, Spin, Typography, Flex, Button } from 'antd';
import {
TrophyOutlined,
FileTextOutlined,
CalendarOutlined,
ShoppingOutlined,
RightOutlined,
} from '@ant-design/icons';
import { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useStatsData } from './useStatsData';
import { articleApi, type ArticleListItem } from '../../../api/health/articles';
import { useCountUp } from '../../../hooks/useCountUp';
export function OperatorDashboard() {
const navigate = useNavigate();
const { pointsStats, offlineEventCount, loading } = useStatsData();
const [topArticles, setTopArticles] = useState<ArticleListItem[]>([]);
const issuedCount = useCountUp(pointsStats?.total_issued ?? 0);
const spentCount = useCountUp(pointsStats?.total_spent ?? 0);
const activeCount = useCountUp(pointsStats?.active_accounts ?? 0);
const offlineCount = useCountUp(offlineEventCount);
const fetchTopArticles = useCallback(async () => {
try {
const result = await articleApi.list({ status: 'published', page: 1, page_size: 5 });
setTopArticles(result.data);
} catch {
// 静默降级
}
}, []);
// fetchTopArticles 内部 async setState外部数据获取非同步派生 state属合理 effect 用法
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { fetchTopArticles(); }, [fetchTopArticles]);
if (loading && !pointsStats) return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
return (
<div>
<Flex justify="space-between" align="center" style={{ marginBottom: 24 }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
</Flex>
<Row gutter={[16, 16]}>
<Col xs={12} sm={6}>
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/points-rules')}>
<Statistic title="积分发放" value={issuedCount} prefix={<TrophyOutlined />} />
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/points-rules')}>
<Statistic title="积分消费" value={spentCount} prefix={<ShoppingOutlined />}
suffix={pointsStats ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{pointsStats.total_issued > 0 ? Math.round(pointsStats.total_spent / pointsStats.total_issued * 100) : 0}%
</Typography.Text>
) : undefined}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/points-rules')}>
<Statistic title="活跃账户" value={activeCount} prefix={<FileTextOutlined />} />
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/offline-events')}>
<Statistic title="线下活动" value={offlineCount} prefix={<CalendarOutlined />} />
</Card>
</Col>
{/* 积分排行 */}
<Col xs={24} md={12}>
<Card
title="积分消费排行"
size="small"
extra={
<Button type="link" size="small" icon={<RightOutlined />} onClick={() => navigate('/health/points-rules')}>
</Button>
}
>
<List
size="small"
dataSource={pointsStats?.top_earners?.slice(0, 5) ?? []}
locale={{ emptyText: '暂无数据' }}
renderItem={(item, idx) => (
<List.Item style={{ cursor: 'pointer' }} onClick={() => navigate(`/health/patients/${item.patient_id}`)}>
<Typography.Text>{idx + 1}. {item.patient_name}</Typography.Text>
<Typography.Text type="secondary">{item.total_earned} </Typography.Text>
</List.Item>
)}
/>
</Card>
</Col>
{/* 热门文章 */}
<Col xs={24} md={12}>
<Card
title="热门文章"
size="small"
extra={
<Button type="link" size="small" icon={<RightOutlined />} onClick={() => navigate('/health/articles')}>
</Button>
}
>
<List
size="small"
dataSource={topArticles}
locale={{ emptyText: '暂无数据' }}
renderItem={(article) => (
<List.Item style={{ padding: '6px 0', cursor: 'pointer' }} onClick={() => navigate(`/health/articles/${article.id}/edit`)}>
<Typography.Text ellipsis style={{ flex: 1, fontSize: 13 }}>{article.title}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
{article.view_count}
</Typography.Text>
</List.Item>
)}
/>
</Card>
</Col>
</Row>
</div>
);
}