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} 占位。
This commit is contained in:
@@ -6,11 +6,13 @@ import {
|
|||||||
MedicineBoxOutlined,
|
MedicineBoxOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useStatsData } from './useStatsData';
|
import { useStatsData } from './useStatsData';
|
||||||
import { useCountUp } from '../../../hooks/useCountUp';
|
import { useCountUp } from '../../../hooks/useCountUp';
|
||||||
import HealthDataCenter from './HealthDataCenter';
|
import HealthDataCenter from './HealthDataCenter';
|
||||||
|
|
||||||
export function AdminDashboard() {
|
export function AdminDashboard() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { patientStats, followUpStats, healthDataStats, dialysisStats, doctorCount, loading } = useStatsData();
|
const { patientStats, followUpStats, healthDataStats, dialysisStats, doctorCount, loading } = useStatsData();
|
||||||
const patientCount = useCountUp(patientStats?.total_patients ?? 0);
|
const patientCount = useCountUp(patientStats?.total_patients ?? 0);
|
||||||
const appointmentCount = useCountUp(healthDataStats?.appointments?.this_month ?? 0);
|
const appointmentCount = useCountUp(healthDataStats?.appointments?.this_month ?? 0);
|
||||||
@@ -18,39 +20,51 @@ export function AdminDashboard() {
|
|||||||
|
|
||||||
if (loading && !patientStats) return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
if (loading && !patientStats) return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
||||||
|
|
||||||
|
const newThisMonth = patientStats?.new_this_month ?? 0;
|
||||||
|
const newThisWeek = patientStats?.new_this_week ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Flex justify="space-between" align="center" style={{ marginBottom: 24 }}>
|
<Flex justify="space-between" align="center" style={{ marginBottom: 24 }}>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>管理中心</Typography.Title>
|
<Typography.Title level={4} style={{ margin: 0 }}>管理中心</Typography.Title>
|
||||||
<Typography.Text type="secondary">数据概览</Typography.Text>
|
<Typography.Text type="secondary">
|
||||||
|
患者增长 {newThisMonth > 0 ? `本月+${newThisMonth}` : ''} · 本周+{newThisWeek} · 活跃 {patientStats?.active_this_month ?? 0}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={12} sm={8} lg={4}>
|
<Col xs={12} sm={8} lg={4}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/patients')}>
|
||||||
<Statistic title="患者总数" value={patientCount} prefix={<TeamOutlined />} />
|
<Statistic title="患者总数" value={patientCount} prefix={<TeamOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} lg={4}>
|
<Col xs={12} sm={8} lg={4}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/appointments')}>
|
||||||
<Statistic title="本月预约" value={appointmentCount} prefix={<CalendarOutlined />} />
|
<Statistic title="本月预约" value={appointmentCount} prefix={<CalendarOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} lg={4}>
|
<Col xs={12} sm={8} lg={4}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/follow-up-tasks')}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="随访完成"
|
title="随访完成"
|
||||||
value={followUpStats?.completion_rate ?? 0}
|
value={followUpStats?.completion_rate ?? 0}
|
||||||
precision={0}
|
precision={0}
|
||||||
suffix="%"
|
suffix="%"
|
||||||
prefix={<SafetyCertificateOutlined />}
|
prefix={<SafetyCertificateOutlined />}
|
||||||
|
suffix={
|
||||||
|
<span style={{ fontSize: 12 }}>
|
||||||
|
% {followUpStats && followUpStats.overdue > 0 && (
|
||||||
|
<Typography.Text type="danger" style={{ fontSize: 11 }}>({followUpStats.overdue}逾期)</Typography.Text>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} lg={4}>
|
<Col xs={12} sm={8} lg={4}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/daily-monitoring')}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="体征上报"
|
title="体征上报"
|
||||||
value={healthDataStats?.vital_signs_report_rate?.report_rate ?? 0}
|
value={healthDataStats?.vital_signs_report_rate?.report_rate ?? 0}
|
||||||
@@ -61,10 +75,20 @@ export function AdminDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} lg={4}>
|
<Col xs={12} sm={8} lg={4}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/doctors')}>
|
||||||
<Statistic title="医护人数" value={doctorCountDisplay} prefix={<UserOutlined />} />
|
<Statistic title="医护人数" value={doctorCountDisplay} prefix={<UserOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={12} sm={8} lg={4}>
|
||||||
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/consultations')}>
|
||||||
|
<Statistic
|
||||||
|
title="咨询待回复"
|
||||||
|
value={healthDataStats ? 0 : 0}
|
||||||
|
prefix={<TeamOutlined />}
|
||||||
|
valueStyle={{ color: '#d97706' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* 健康数据中心 Tab */}
|
{/* 健康数据中心 Tab */}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { Row, Col, Card, Statistic, List, Tag, Spin, Typography, Flex, Space, Bu
|
|||||||
import {
|
import {
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
MessageOutlined,
|
MessageOutlined,
|
||||||
SafetyCertificateOutlined,
|
|
||||||
MedicineBoxOutlined,
|
MedicineBoxOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
ArrowUpOutlined,
|
ArrowUpOutlined,
|
||||||
|
ArrowDownOutlined,
|
||||||
AlertOutlined,
|
AlertOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@@ -137,7 +138,7 @@ export function DoctorDashboard() {
|
|||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/patients')}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="我的患者"
|
title="我的患者"
|
||||||
value={myPatientsCount}
|
value={myPatientsCount}
|
||||||
@@ -151,19 +152,22 @@ export function DoctorDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/follow-up-tasks')}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="随访完成率"
|
title="今日预约"
|
||||||
value={p?.follow_up_rate ?? 0}
|
value={p?.today_appointments ?? 0}
|
||||||
precision={0}
|
prefix={<CalendarOutlined />}
|
||||||
suffix="%"
|
suffix={p?.yesterday_today_appointments != null ? (() => {
|
||||||
prefix={<SafetyCertificateOutlined />}
|
const diff = (p.today_appointments ?? 0) - (p.yesterday_today_appointments ?? 0);
|
||||||
styles={{ content: { color: (p?.follow_up_rate ?? 0) >= 80 ? '#3f8600' : '#cf1322' } }}
|
if (diff > 0) return <Typography.Text type="success" style={{ fontSize: 12 }}><ArrowUpOutlined /> {diff}</Typography.Text>;
|
||||||
|
if (diff < 0) return <Typography.Text type="danger" style={{ fontSize: 12 }}><ArrowDownOutlined /> {Math.abs(diff)}</Typography.Text>;
|
||||||
|
return null;
|
||||||
|
})() : undefined}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/consultations')}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="本月咨询"
|
title="本月咨询"
|
||||||
value={consultationsCount}
|
value={consultationsCount}
|
||||||
@@ -177,7 +181,7 @@ export function DoctorDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/daily-monitoring')}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="体征上报率"
|
title="体征上报率"
|
||||||
value={p?.vital_signs_report_rate ?? 0}
|
value={p?.vital_signs_report_rate ?? 0}
|
||||||
@@ -192,13 +196,18 @@ export function DoctorDashboard() {
|
|||||||
{/* 化验审核 */}
|
{/* 化验审核 */}
|
||||||
{p && p.pending_lab_reviews > 0 && (
|
{p && p.pending_lab_reviews > 0 && (
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Card title={`化验审核 (${p.pending_lab_reviews}待审)`} size="small">
|
<Card
|
||||||
<List
|
title={`化验审核 (${p.pending_lab_reviews}待审)`}
|
||||||
size="small"
|
size="small"
|
||||||
dataSource={[]}
|
extra={
|
||||||
locale={{ emptyText: '暂无待审核化验' }}
|
<Button type="link" size="small" icon={<RightOutlined />} onClick={() => navigate('/health/patients')}>
|
||||||
renderItem={() => <List.Item />}
|
查看待审
|
||||||
/>
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
您有 {p.pending_lab_reviews} 份化验报告待审核,请及时处理。
|
||||||
|
</Typography.Text>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
@@ -211,7 +220,7 @@ export function DoctorDashboard() {
|
|||||||
dataSource={activeConsultations}
|
dataSource={activeConsultations}
|
||||||
locale={{ emptyText: '暂无未读消息' }}
|
locale={{ emptyText: '暂无未读消息' }}
|
||||||
renderItem={(session) => (
|
renderItem={(session) => (
|
||||||
<List.Item style={{ padding: '6px 0' }}>
|
<List.Item style={{ padding: '6px 0', cursor: 'pointer' }} onClick={() => navigate(`/health/consultations/${session.id}`)}>
|
||||||
<Space>
|
<Space>
|
||||||
<Typography.Text strong style={{ fontSize: 13 }}>{session.patient_name ?? '患者'}</Typography.Text>
|
<Typography.Text strong style={{ fontSize: 13 }}>{session.patient_name ?? '患者'}</Typography.Text>
|
||||||
<Tag color={session.status === 'active' ? 'green' : 'default'}>{session.status === 'active' ? '进行中' : session.status}</Tag>
|
<Tag color={session.status === 'active' ? 'green' : 'default'}>{session.status === 'active' ? '进行中' : session.status}</Tag>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Row, Col, Card, Statistic, Progress, List, Spin, Typography, Flex, Space } from 'antd';
|
import { Row, Col, Card, Statistic, Progress, List, Spin, Typography, Flex, Space } from 'antd';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
@@ -11,6 +12,7 @@ import { followUpApi, type FollowUpTask } from '../../../api/health/followUp';
|
|||||||
import { useCountUp } from '../../../hooks/useCountUp';
|
import { useCountUp } from '../../../hooks/useCountUp';
|
||||||
|
|
||||||
export function NurseDashboard() {
|
export function NurseDashboard() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [personal, setPersonal] = useState<PersonalStats | null>(null);
|
const [personal, setPersonal] = useState<PersonalStats | null>(null);
|
||||||
const [followUpTasks, setFollowUpTasks] = useState<FollowUpTask[]>([]);
|
const [followUpTasks, setFollowUpTasks] = useState<FollowUpTask[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -66,7 +68,7 @@ export function NurseDashboard() {
|
|||||||
{p.abnormal_vital_signs} 位患者体征异常
|
{p.abnormal_vital_signs} 位患者体征异常
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Typography.Link>查看全部 →</Typography.Link>
|
<Typography.Link onClick={() => navigate('/health/alert-dashboard')}>查看全部 →</Typography.Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import { Row, Col, Card, Statistic, List, Spin, Typography, Flex } from 'antd';
|
import { Row, Col, Card, Statistic, List, Spin, Typography, Flex, Button } from 'antd';
|
||||||
import {
|
import {
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
ShoppingOutlined,
|
ShoppingOutlined,
|
||||||
|
RightOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useStatsData } from './useStatsData';
|
import { useStatsData } from './useStatsData';
|
||||||
import { articleApi, type ArticleListItem } from '../../../api/health/articles';
|
import { articleApi, type ArticleListItem } from '../../../api/health/articles';
|
||||||
import { useCountUp } from '../../../hooks/useCountUp';
|
import { useCountUp } from '../../../hooks/useCountUp';
|
||||||
|
|
||||||
export function OperatorDashboard() {
|
export function OperatorDashboard() {
|
||||||
const { pointsStats, loading } = useStatsData();
|
const navigate = useNavigate();
|
||||||
|
const { pointsStats, offlineEventCount, loading } = useStatsData();
|
||||||
const [topArticles, setTopArticles] = useState<ArticleListItem[]>([]);
|
const [topArticles, setTopArticles] = useState<ArticleListItem[]>([]);
|
||||||
const issuedCount = useCountUp(pointsStats?.total_issued ?? 0);
|
const issuedCount = useCountUp(pointsStats?.total_issued ?? 0);
|
||||||
const spentCount = useCountUp(pointsStats?.total_spent ?? 0);
|
const spentCount = useCountUp(pointsStats?.total_spent ?? 0);
|
||||||
const activeCount = useCountUp(pointsStats?.active_accounts ?? 0);
|
const activeCount = useCountUp(pointsStats?.active_accounts ?? 0);
|
||||||
|
const offlineCount = useCountUp(offlineEventCount);
|
||||||
|
|
||||||
const fetchTopArticles = useCallback(async () => {
|
const fetchTopArticles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -26,6 +30,8 @@ export function OperatorDashboard() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// fetchTopArticles 内部 async setState(外部数据获取),非同步派生 state,属合理 effect 用法
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { fetchTopArticles(); }, [fetchTopArticles]);
|
useEffect(() => { fetchTopArticles(); }, [fetchTopArticles]);
|
||||||
|
|
||||||
if (loading && !pointsStats) return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
if (loading && !pointsStats) return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
||||||
@@ -41,12 +47,12 @@ export function OperatorDashboard() {
|
|||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/points-rules')}>
|
||||||
<Statistic title="积分发放" value={issuedCount} prefix={<TrophyOutlined />} />
|
<Statistic title="积分发放" value={issuedCount} prefix={<TrophyOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/points-rules')}>
|
||||||
<Statistic title="积分消费" value={spentCount} prefix={<ShoppingOutlined />}
|
<Statistic title="积分消费" value={spentCount} prefix={<ShoppingOutlined />}
|
||||||
suffix={pointsStats ? (
|
suffix={pointsStats ? (
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
@@ -57,26 +63,34 @@ export function OperatorDashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/points-rules')}>
|
||||||
<Statistic title="活跃账户" value={activeCount} prefix={<FileTextOutlined />} />
|
<Statistic title="活跃账户" value={activeCount} prefix={<FileTextOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" style={{ cursor: 'pointer' }} onClick={() => navigate('/health/offline-events')}>
|
||||||
<Statistic title="线下活动" value={0} prefix={<CalendarOutlined />} />
|
<Statistic title="线下活动" value={offlineCount} prefix={<CalendarOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{/* 积分排行 */}
|
{/* 积分排行 */}
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Card title="积分消费排行" size="small">
|
<Card
|
||||||
|
title="积分消费排行"
|
||||||
|
size="small"
|
||||||
|
extra={
|
||||||
|
<Button type="link" size="small" icon={<RightOutlined />} onClick={() => navigate('/health/points-rules')}>
|
||||||
|
查看全部
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<List
|
<List
|
||||||
size="small"
|
size="small"
|
||||||
dataSource={pointsStats?.top_earners?.slice(0, 5) ?? []}
|
dataSource={pointsStats?.top_earners?.slice(0, 5) ?? []}
|
||||||
locale={{ emptyText: '暂无数据' }}
|
locale={{ emptyText: '暂无数据' }}
|
||||||
renderItem={(item, idx) => (
|
renderItem={(item, idx) => (
|
||||||
<List.Item>
|
<List.Item style={{ cursor: 'pointer' }} onClick={() => navigate(`/health/patients/${item.patient_id}`)}>
|
||||||
<Typography.Text>{idx + 1}. {item.patient_id?.slice(0, 8) ?? '未知'}</Typography.Text>
|
<Typography.Text>{idx + 1}. {item.patient_name}</Typography.Text>
|
||||||
<Typography.Text type="secondary">{item.total_earned} 分</Typography.Text>
|
<Typography.Text type="secondary">{item.total_earned} 分</Typography.Text>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
@@ -86,13 +100,21 @@ export function OperatorDashboard() {
|
|||||||
|
|
||||||
{/* 热门文章 */}
|
{/* 热门文章 */}
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Card title="热门文章" size="small">
|
<Card
|
||||||
|
title="热门文章"
|
||||||
|
size="small"
|
||||||
|
extra={
|
||||||
|
<Button type="link" size="small" icon={<RightOutlined />} onClick={() => navigate('/health/articles')}>
|
||||||
|
内容管理
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<List
|
<List
|
||||||
size="small"
|
size="small"
|
||||||
dataSource={topArticles}
|
dataSource={topArticles}
|
||||||
locale={{ emptyText: '暂无数据' }}
|
locale={{ emptyText: '暂无数据' }}
|
||||||
renderItem={(article) => (
|
renderItem={(article) => (
|
||||||
<List.Item style={{ padding: '6px 0' }}>
|
<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 ellipsis style={{ flex: 1, fontSize: 13 }}>{article.title}</Typography.Text>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
|
||||||
{article.view_count} 次阅读
|
{article.view_count} 次阅读
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface StatsData {
|
|||||||
healthDataStats: HealthDataStats | null;
|
healthDataStats: HealthDataStats | null;
|
||||||
dialysisStats: DialysisStatistics | null;
|
dialysisStats: DialysisStatistics | null;
|
||||||
doctorCount: number;
|
doctorCount: number;
|
||||||
|
offlineEventCount: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
@@ -40,6 +41,7 @@ export function useStatsData(): StatsData {
|
|||||||
const [healthDataStats, setHealthDataStats] = useState<HealthDataStats | null>(null);
|
const [healthDataStats, setHealthDataStats] = useState<HealthDataStats | null>(null);
|
||||||
const [dialysisStats, setDialysisStats] = useState<DialysisStatistics | null>(null);
|
const [dialysisStats, setDialysisStats] = useState<DialysisStatistics | null>(null);
|
||||||
const [doctorCount, setDoctorCount] = useState(0);
|
const [doctorCount, setDoctorCount] = useState(0);
|
||||||
|
const [offlineEventCount, setOfflineEventCount] = useState(0);
|
||||||
|
|
||||||
const fetchAllStats = useCallback(async () => {
|
const fetchAllStats = useCallback(async () => {
|
||||||
// 缓存未过期,直接使用
|
// 缓存未过期,直接使用
|
||||||
@@ -52,6 +54,7 @@ export function useStatsData(): StatsData {
|
|||||||
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
||||||
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
||||||
setDoctorCount(c.doctorCount as number);
|
setDoctorCount(c.doctorCount as number);
|
||||||
|
setOfflineEventCount(c.offlineEventCount as number);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,6 +69,7 @@ export function useStatsData(): StatsData {
|
|||||||
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
||||||
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
||||||
setDoctorCount(c.doctorCount as number);
|
setDoctorCount(c.doctorCount as number);
|
||||||
|
setOfflineEventCount(c.offlineEventCount as number);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,6 +90,7 @@ export function useStatsData(): StatsData {
|
|||||||
healthDataStats: null,
|
healthDataStats: null,
|
||||||
dialysisStats: null,
|
dialysisStats: null,
|
||||||
doctorCount: 0,
|
doctorCount: 0,
|
||||||
|
offlineEventCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryFetch = async <T,>(fn: () => Promise<T>, key: string, label: string) => {
|
const tryFetch = async <T,>(fn: () => Promise<T>, key: string, label: string) => {
|
||||||
@@ -110,14 +115,19 @@ export function useStatsData(): StatsData {
|
|||||||
'doctorCount',
|
'doctorCount',
|
||||||
'医护',
|
'医护',
|
||||||
),
|
),
|
||||||
|
tryFetch(
|
||||||
|
async () => { const r = await pointsApi.listOfflineEvents({ page: 1, page_size: 1 }); return r.total; },
|
||||||
|
'offlineEventCount',
|
||||||
|
'线下活动',
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!hasAnyError || errors.length < 7) {
|
if (!hasAnyError || errors.length < 8) {
|
||||||
cachedStats = results;
|
cachedStats = results;
|
||||||
cachedAt = Date.now();
|
cachedAt = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAnyError && errors.length === 7) {
|
if (hasAnyError && errors.length === 8) {
|
||||||
setError('加载统计数据失败');
|
setError('加载统计数据失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +143,7 @@ export function useStatsData(): StatsData {
|
|||||||
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
||||||
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
||||||
setDoctorCount(c.doctorCount as number);
|
setDoctorCount(c.doctorCount as number);
|
||||||
|
setOfflineEventCount(c.offlineEventCount as number);
|
||||||
} finally {
|
} finally {
|
||||||
fetchPromise = null;
|
fetchPromise = null;
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -144,7 +155,7 @@ export function useStatsData(): StatsData {
|
|||||||
}, [fetchAllStats]);
|
}, [fetchAllStats]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patientStats, consultationStats, followUpStats, pointsStats, healthDataStats, dialysisStats, doctorCount,
|
patientStats, consultationStats, followUpStats, pointsStats, healthDataStats, dialysisStats, doctorCount, offlineEventCount,
|
||||||
loading, error, refresh: fetchAllStats,
|
loading, error, refresh: fetchAllStats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user