feat(web): 区域发病统计图表与蚕房区域字段(#22)
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 } });
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user