feat(web): 区域发病统计图表与蚕房区域字段(#22)
This commit is contained in:
@@ -5,6 +5,7 @@ export interface SilkwormHouse {
|
||||
name: string;
|
||||
code?: string;
|
||||
location?: string;
|
||||
region?: string;
|
||||
description?: string;
|
||||
status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm';
|
||||
capacity?: number;
|
||||
|
||||
@@ -36,3 +36,12 @@ export const getTraceChecklist = (id: string) =>
|
||||
get<ChecklistItem[]>(`/trace-records/${id}/checklist`);
|
||||
export const submitTraceChecklist = (id: string, answers: { key: string; answer: string }[]) =>
|
||||
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: '名称', dataIndex: 'name' },
|
||||
{ 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: '阶段',
|
||||
@@ -111,6 +112,9 @@ export default function SilkwormPage() {
|
||||
<Form.Item label="位置" name="location" rules={[{ required: true, message: '请输入位置' }]}>
|
||||
<Input placeholder="如:1号厂房东侧" />
|
||||
</Form.Item>
|
||||
<Form.Item label="区域" name="region">
|
||||
<Input placeholder="如:XX镇 / XX村(用于区域发病统计)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="容量(盒)" name="capacity" rules={[{ required: true, message: '请输入容量' }]}>
|
||||
<Input type="number" min={0} placeholder="蚕盒数量" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Button, Drawer, Form, Input, Modal, Popconfirm, Radio, Select, Space, Tag, Typography, message,
|
||||
} from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
listTraceRecords,
|
||||
@@ -13,8 +14,10 @@ import {
|
||||
autoTrace,
|
||||
getTraceChecklist,
|
||||
submitTraceChecklist,
|
||||
getRegionStats,
|
||||
type TraceRecord,
|
||||
type ChecklistItem,
|
||||
type RegionStat,
|
||||
} from '../dal/trace';
|
||||
import { authService } from '../services/auth';
|
||||
|
||||
@@ -28,6 +31,7 @@ const STATUS_LABELS: Record<string, { color: string; text: string }> = {
|
||||
const canWrite = () => authService.hasPermission('trace:write');
|
||||
|
||||
export default function TracesPage() {
|
||||
const [regionStats, setRegionStats] = useState<RegionStat[]>([]);
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createForm] = Form.useForm<Partial<TraceRecord>>();
|
||||
@@ -35,6 +39,38 @@ export default function TracesPage() {
|
||||
const [checklist, setChecklist] = useState<ChecklistItem[]>([]);
|
||||
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) => {
|
||||
setDetail(r);
|
||||
if (r.status !== 'archived') {
|
||||
@@ -116,6 +152,16 @@ export default function TracesPage() {
|
||||
|
||||
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>
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
|
||||
Reference in New Issue
Block a user