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 { 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 { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import { listHouses, createHouse, updateHouse, deleteHouse, type SilkwormHouse } from '../dal/silkworm';
|
||||
import { getHealthProfile, type HealthProfile } from '../dal/health';
|
||||
|
||||
export default function SilkwormPage() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm<Partial<SilkwormHouse>>();
|
||||
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>[] = [
|
||||
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 },
|
||||
@@ -44,6 +48,20 @@ export default function SilkwormPage() {
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, r) => [
|
||||
<a
|
||||
key="health"
|
||||
onClick={async () => {
|
||||
setProfileOpen(true);
|
||||
setProfileLoading(true);
|
||||
try {
|
||||
setProfile(await getHealthProfile(r.id));
|
||||
} finally {
|
||||
setProfileLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
健康画像
|
||||
</a>,
|
||||
<a
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
@@ -131,6 +149,42 @@ export default function SilkwormPage() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</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 RegionStat,
|
||||
} from '../dal/trace';
|
||||
import { getMonthlyStats } from '../dal/health';
|
||||
import { authService } from '../services/auth';
|
||||
|
||||
const STATUS_LABELS: Record<string, { color: string; text: string }> = {
|
||||
@@ -32,6 +33,7 @@ const canWrite = () => authService.hasPermission('trace:write');
|
||||
|
||||
export default function TracesPage() {
|
||||
const [regionStats, setRegionStats] = useState<RegionStat[]>([]);
|
||||
const [monthlyStats, setMonthlyStats] = useState<{ month: string; total: number }[]>([]);
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createForm] = Form.useForm<Partial<TraceRecord>>();
|
||||
@@ -43,6 +45,9 @@ export default function TracesPage() {
|
||||
getRegionStats(90)
|
||||
.then(setRegionStats)
|
||||
.catch(() => {});
|
||||
getMonthlyStats()
|
||||
.then((res) => setMonthlyStats(res.stats.map((s) => ({ month: s.month, total: s.total }))))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
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) => {
|
||||
setDetail(r);
|
||||
if (r.status !== 'archived') {
|
||||
@@ -162,6 +175,11 @@ export default function TracesPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{monthlyStats.length > 0 ? (
|
||||
<div style={{ background: '#fff', borderRadius: 8, padding: 8, marginBottom: 16 }}>
|
||||
<ReactECharts option={monthlyOption} style={{ height: 240 }} />
|
||||
</div>
|
||||
) : null}
|
||||
<ProTable<TraceRecord>
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
|
||||
Reference in New Issue
Block a user