feat(web): 蚕房健康画像与年度发病规律图表(#24)
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { get } from '../api/http';
|
||||||
|
|
||||||
|
export interface HealthProfile {
|
||||||
|
roomId: string;
|
||||||
|
roomName: string;
|
||||||
|
score: number;
|
||||||
|
grade: string;
|
||||||
|
riskDistribution: { green: number; yellow: number; orange: number; red: number };
|
||||||
|
lampResults: { total: number; positive: number };
|
||||||
|
events: { consultations: number; traces: number };
|
||||||
|
batch?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHealthProfile = (roomId: string) =>
|
||||||
|
get<HealthProfile>(`/rooms/${roomId}/health-profile`);
|
||||||
|
|
||||||
|
export const getMonthlyStats = (year?: number) =>
|
||||||
|
get<{ year: number; stats: { month: string; total: number; diseases: Record<string, number> }[] }>(
|
||||||
|
'/trace-records/monthly-stats',
|
||||||
|
{ params: { year } },
|
||||||
|
);
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Form, Input, Modal, Popconfirm, Select, Tag, message } from 'antd';
|
import { Button, Descriptions, Drawer, Form, Input, Modal, Popconfirm, Select, Tag, message, Progress } from 'antd';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
import { listHouses, createHouse, updateHouse, deleteHouse, type SilkwormHouse } from '../dal/silkworm';
|
import { listHouses, createHouse, updateHouse, deleteHouse, type SilkwormHouse } from '../dal/silkworm';
|
||||||
|
import { getHealthProfile, type HealthProfile } from '../dal/health';
|
||||||
|
|
||||||
export default function SilkwormPage() {
|
export default function SilkwormPage() {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [form] = Form.useForm<Partial<SilkwormHouse>>();
|
const [form] = Form.useForm<Partial<SilkwormHouse>>();
|
||||||
const [editing, setEditing] = useState<SilkwormHouse | null>(null);
|
const [editing, setEditing] = useState<SilkwormHouse | null>(null);
|
||||||
|
const [profileOpen, setProfileOpen] = useState(false);
|
||||||
|
const [profile, setProfile] = useState<HealthProfile | null>(null);
|
||||||
|
const [profileLoading, setProfileLoading] = useState(false);
|
||||||
|
|
||||||
const columns: ProColumns<SilkwormHouse>[] = [
|
const columns: ProColumns<SilkwormHouse>[] = [
|
||||||
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 },
|
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 },
|
||||||
@@ -44,6 +48,20 @@ export default function SilkwormPage() {
|
|||||||
title: '操作',
|
title: '操作',
|
||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
render: (_, r) => [
|
render: (_, r) => [
|
||||||
|
<a
|
||||||
|
key="health"
|
||||||
|
onClick={async () => {
|
||||||
|
setProfileOpen(true);
|
||||||
|
setProfileLoading(true);
|
||||||
|
try {
|
||||||
|
setProfile(await getHealthProfile(r.id));
|
||||||
|
} finally {
|
||||||
|
setProfileLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
健康画像
|
||||||
|
</a>,
|
||||||
<a
|
<a
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -131,6 +149,42 @@ export default function SilkwormPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Drawer title={profile ? `${profile.roomName} - 健康画像` : '健康画像'} open={profileOpen} width={520} onClose={() => setProfileOpen(false)}>
|
||||||
|
{profileLoading ? <div>加载中...</div> : null}
|
||||||
|
{profile ? (
|
||||||
|
<div>
|
||||||
|
<Progress
|
||||||
|
type="dashboard"
|
||||||
|
percent={Math.round(profile.score)}
|
||||||
|
format={() => `${Math.round(profile.score)}分`}
|
||||||
|
strokeColor={profile.score >= 85 ? '#52c41a' : profile.score >= 70 ? '#10b981' : profile.score >= 55 ? '#faad14' : '#f5222d'}
|
||||||
|
/>
|
||||||
|
<Descriptions column={1} style={{ marginTop: 16 }}>
|
||||||
|
<Descriptions.Item label="综合评级">
|
||||||
|
<Tag color={profile.grade === '优' ? 'green' : profile.grade === '良' ? 'blue' : profile.grade === '中' ? 'gold' : 'red'}>
|
||||||
|
{profile.grade}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="巡检风险分布">
|
||||||
|
绿 {profile.riskDistribution.green} / 黄 {profile.riskDistribution.yellow} / 橙{' '}
|
||||||
|
{profile.riskDistribution.orange} / 红 {profile.riskDistribution.red}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="检测结果(近30天)">
|
||||||
|
共 {profile.lampResults.total} 项,阳性 {profile.lampResults.positive} 项
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="事件">
|
||||||
|
会诊 {profile.events.consultations} 次 / 溯源 {profile.events.traces} 次
|
||||||
|
</Descriptions.Item>
|
||||||
|
{profile.batch ? (
|
||||||
|
<Descriptions.Item label="当前批次">
|
||||||
|
{profile.batch.name}
|
||||||
|
{profile.batch.instar ? `(${profile.batch.instar}龄)` : ''}
|
||||||
|
</Descriptions.Item>
|
||||||
|
) : null}
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
type ChecklistItem,
|
type ChecklistItem,
|
||||||
type RegionStat,
|
type RegionStat,
|
||||||
} from '../dal/trace';
|
} from '../dal/trace';
|
||||||
|
import { getMonthlyStats } from '../dal/health';
|
||||||
import { authService } from '../services/auth';
|
import { authService } from '../services/auth';
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, { color: string; text: string }> = {
|
const STATUS_LABELS: Record<string, { color: string; text: string }> = {
|
||||||
@@ -32,6 +33,7 @@ const canWrite = () => authService.hasPermission('trace:write');
|
|||||||
|
|
||||||
export default function TracesPage() {
|
export default function TracesPage() {
|
||||||
const [regionStats, setRegionStats] = useState<RegionStat[]>([]);
|
const [regionStats, setRegionStats] = useState<RegionStat[]>([]);
|
||||||
|
const [monthlyStats, setMonthlyStats] = useState<{ month: string; total: number }[]>([]);
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [createForm] = Form.useForm<Partial<TraceRecord>>();
|
const [createForm] = Form.useForm<Partial<TraceRecord>>();
|
||||||
@@ -43,6 +45,9 @@ export default function TracesPage() {
|
|||||||
getRegionStats(90)
|
getRegionStats(90)
|
||||||
.then(setRegionStats)
|
.then(setRegionStats)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
getMonthlyStats()
|
||||||
|
.then((res) => setMonthlyStats(res.stats.map((s) => ({ month: s.month, total: s.total }))))
|
||||||
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const barOption = {
|
const barOption = {
|
||||||
@@ -71,6 +76,14 @@ export default function TracesPage() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const monthlyOption = {
|
||||||
|
title: { text: '年度发病规律' },
|
||||||
|
tooltip: {},
|
||||||
|
xAxis: { type: 'category', data: monthlyStats.map((s) => s.month) },
|
||||||
|
yAxis: { type: 'value', minInterval: 1 },
|
||||||
|
series: [{ type: 'line', data: monthlyStats.map((s) => s.total), smooth: true, areaStyle: {} }],
|
||||||
|
};
|
||||||
|
|
||||||
const openDetail = async (r: TraceRecord) => {
|
const openDetail = async (r: TraceRecord) => {
|
||||||
setDetail(r);
|
setDetail(r);
|
||||||
if (r.status !== 'archived') {
|
if (r.status !== 'archived') {
|
||||||
@@ -162,6 +175,11 @@ export default function TracesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{monthlyStats.length > 0 ? (
|
||||||
|
<div style={{ background: '#fff', borderRadius: 8, padding: 8, marginBottom: 16 }}>
|
||||||
|
<ReactECharts option={monthlyOption} style={{ height: 240 }} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ProTable<TraceRecord>
|
<ProTable<TraceRecord>
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
Reference in New Issue
Block a user