feat(web): 区域发病统计图表与蚕房区域字段(#22)

This commit is contained in:
weijuesen
2026-08-13 08:54:08 +08:00
parent 7949f90c0c
commit 9c5af92619
4 changed files with 61 additions and 1 deletions
+1
View File
@@ -5,6 +5,7 @@ export interface SilkwormHouse {
name: string; name: string;
code?: string; code?: string;
location?: string; location?: string;
region?: string;
description?: string; description?: string;
status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm'; status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm';
capacity?: number; capacity?: number;
+9
View File
@@ -36,3 +36,12 @@ export const getTraceChecklist = (id: string) =>
get<ChecklistItem[]>(`/trace-records/${id}/checklist`); get<ChecklistItem[]>(`/trace-records/${id}/checklist`);
export const submitTraceChecklist = (id: string, answers: { key: string; answer: string }[]) => export const submitTraceChecklist = (id: string, answers: { key: string; answer: string }[]) =>
post<TraceRecord>(`/trace-records/${id}/checklist`, { answers }); post<TraceRecord>(`/trace-records/${id}/checklist`, { answers });
export interface RegionStat {
region: string;
total: number;
diseases: Record<string, number>;
}
export const getRegionStats = (days = 90, disease?: string) =>
get<RegionStat[]>('/trace-records/region-stats', { params: { days, disease } });
+4
View File
@@ -13,6 +13,7 @@ export default function SilkwormPage() {
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 }, { title: '序号', valueType: 'indexBorder', search: false, width: 60 },
{ title: '名称', dataIndex: 'name' }, { title: '名称', dataIndex: 'name' },
{ title: '位置', dataIndex: 'location', search: false, render: (_, r) => r.location || '-' }, { title: '位置', dataIndex: 'location', search: false, render: (_, r) => r.location || '-' },
{ title: '区域', dataIndex: 'region', search: false, render: (_, r) => r.region || '-' },
{ title: '容量(盒)', dataIndex: 'capacity', search: false, render: (_, r) => r.capacity ?? '-' }, { title: '容量(盒)', dataIndex: 'capacity', search: false, render: (_, r) => r.capacity ?? '-' },
{ {
title: '阶段', title: '阶段',
@@ -111,6 +112,9 @@ export default function SilkwormPage() {
<Form.Item label="位置" name="location" rules={[{ required: true, message: '请输入位置' }]}> <Form.Item label="位置" name="location" rules={[{ required: true, message: '请输入位置' }]}>
<Input placeholder="如:1号厂房东侧" /> <Input placeholder="如:1号厂房东侧" />
</Form.Item> </Form.Item>
<Form.Item label="区域" name="region">
<Input placeholder="如:XX镇 / XX村(用于区域发病统计)" />
</Form.Item>
<Form.Item label="容量(盒)" name="capacity" rules={[{ required: true, message: '请输入容量' }]}> <Form.Item label="容量(盒)" name="capacity" rules={[{ required: true, message: '请输入容量' }]}>
<Input type="number" min={0} placeholder="蚕盒数量" /> <Input type="number" min={0} placeholder="蚕盒数量" />
</Form.Item> </Form.Item>
+47 -1
View File
@@ -1,9 +1,10 @@
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { import {
Button, Drawer, Form, Input, Modal, Popconfirm, Radio, Select, Space, Tag, Typography, message, Button, Drawer, Form, Input, Modal, Popconfirm, Radio, Select, Space, Tag, Typography, message,
} from 'antd'; } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
import ReactECharts from 'echarts-for-react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import {
listTraceRecords, listTraceRecords,
@@ -13,8 +14,10 @@ import {
autoTrace, autoTrace,
getTraceChecklist, getTraceChecklist,
submitTraceChecklist, submitTraceChecklist,
getRegionStats,
type TraceRecord, type TraceRecord,
type ChecklistItem, type ChecklistItem,
type RegionStat,
} from '../dal/trace'; } from '../dal/trace';
import { authService } from '../services/auth'; import { authService } from '../services/auth';
@@ -28,6 +31,7 @@ const STATUS_LABELS: Record<string, { color: string; text: string }> = {
const canWrite = () => authService.hasPermission('trace:write'); const canWrite = () => authService.hasPermission('trace:write');
export default function TracesPage() { export default function TracesPage() {
const [regionStats, setRegionStats] = useState<RegionStat[]>([]);
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>>();
@@ -35,6 +39,38 @@ export default function TracesPage() {
const [checklist, setChecklist] = useState<ChecklistItem[]>([]); const [checklist, setChecklist] = useState<ChecklistItem[]>([]);
const [answers, setAnswers] = useState<Record<string, string>>({}); const [answers, setAnswers] = useState<Record<string, string>>({});
useEffect(() => {
getRegionStats(90)
.then(setRegionStats)
.catch(() => {});
}, []);
const barOption = {
title: { text: '区域发病统计(近 90 天)' },
tooltip: {},
xAxis: { type: 'category', data: regionStats.map((s) => s.region) },
yAxis: { type: 'value', minInterval: 1 },
series: [{ type: 'bar', data: regionStats.map((s) => s.total), itemStyle: { color: '#fa8c16' } }],
};
const diseaseMap: Record<string, number> = {};
regionStats.forEach((s) => {
Object.entries(s.diseases).forEach(([d, n]) => {
diseaseMap[d] = (diseaseMap[d] || 0) + n;
});
});
const pieOption = {
title: { text: '病种分布' },
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['35%', '65%'],
data: Object.entries(diseaseMap).map(([name, value]) => ({ name, value })),
},
],
};
const openDetail = async (r: TraceRecord) => { const openDetail = async (r: TraceRecord) => {
setDetail(r); setDetail(r);
if (r.status !== 'archived') { if (r.status !== 'archived') {
@@ -116,6 +152,16 @@ export default function TracesPage() {
return ( return (
<> <>
{regionStats.length > 0 ? (
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
<div style={{ flex: 2, background: '#fff', borderRadius: 8, padding: 8 }}>
<ReactECharts option={barOption} style={{ height: 260 }} />
</div>
<div style={{ flex: 1, background: '#fff', borderRadius: 8, padding: 8 }}>
<ReactECharts option={pieOption} style={{ height: 260 }} />
</div>
</div>
) : null}
<ProTable<TraceRecord> <ProTable<TraceRecord>
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"