feat(web/miniapp): 批次/蚕匾管理与蚕房详情展示(#7)
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
import { get } from './request';
|
||||||
|
|
||||||
|
export interface Tray {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code?: string;
|
||||||
|
position?: string;
|
||||||
|
roomId: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Batch {
|
||||||
|
id: string;
|
||||||
|
roomId: string;
|
||||||
|
name: string;
|
||||||
|
variety?: string;
|
||||||
|
source?: string;
|
||||||
|
instar?: number;
|
||||||
|
status: string;
|
||||||
|
enteredAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTrays = (roomId: string) => get<Tray[]>('/trays', { roomId });
|
||||||
|
export const getBatches = (roomId: string) => get<Batch[]>('/batches', { roomId });
|
||||||
|
|
||||||
|
export const batchStatusLabel = (status: string) => {
|
||||||
|
const labels: Record<string, string> = { rearing: '饲养中', mounted: '已上蔟', finished: '已结束' };
|
||||||
|
return labels[status] || status;
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import styles from './index.module.scss';
|
|||||||
import { getRoomById } from '@/api/rooms';
|
import { getRoomById } from '@/api/rooms';
|
||||||
import { getDevices } from '@/api/devices';
|
import { getDevices } from '@/api/devices';
|
||||||
import { getLatestTelemetry, fetchTrend } from '@/api/telemetry';
|
import { getLatestTelemetry, fetchTrend } from '@/api/telemetry';
|
||||||
|
import { getTrays, getBatches, batchStatusLabel, type Tray, type Batch } from '@/api/trayBatch';
|
||||||
import MiniChart from '@/components/MiniChart';
|
import MiniChart from '@/components/MiniChart';
|
||||||
import {
|
import {
|
||||||
roomStatusLabel,
|
roomStatusLabel,
|
||||||
@@ -24,6 +25,8 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
const [devices, setDevices] = useState<Device[]>([]);
|
const [devices, setDevices] = useState<Device[]>([]);
|
||||||
const [telemetryMap, setTelemetryMap] = useState<Record<string, TelemetryRecord[]>>({});
|
const [telemetryMap, setTelemetryMap] = useState<Record<string, TelemetryRecord[]>>({});
|
||||||
const [trend, setTrend] = useState<TrendPoint[]>([]);
|
const [trend, setTrend] = useState<TrendPoint[]>([]);
|
||||||
|
const [trays, setTrays] = useState<Tray[]>([]);
|
||||||
|
const [batches, setBatches] = useState<Batch[]>([]);
|
||||||
const [trendLoading, setTrendLoading] = useState(false);
|
const [trendLoading, setTrendLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -37,12 +40,14 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setError('');
|
setError('');
|
||||||
const [roomRes, devicesRes] = await Promise.all([
|
const [roomRes, devicesRes, traysRes, batchesRes] = await Promise.all([
|
||||||
getRoomById(roomId).catch((e) => {
|
getRoomById(roomId).catch((e) => {
|
||||||
console.error('[RoomDetail] 获取蚕房失败:', e);
|
console.error('[RoomDetail] 获取蚕房失败:', e);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
getDevices().catch(() => [] as Device[]),
|
getDevices().catch(() => [] as Device[]),
|
||||||
|
getTrays(roomId).catch(() => [] as Tray[]),
|
||||||
|
getBatches(roomId).catch(() => [] as Batch[]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (roomRes) {
|
if (roomRes) {
|
||||||
@@ -51,6 +56,8 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
const roomDevices = devicesRes.filter((d) => d.roomId === roomId);
|
const roomDevices = devicesRes.filter((d) => d.roomId === roomId);
|
||||||
setDevices(roomDevices);
|
setDevices(roomDevices);
|
||||||
|
setTrays(traysRes);
|
||||||
|
setBatches(batchesRes);
|
||||||
|
|
||||||
// 获取每个设备的最新遥测
|
// 获取每个设备的最新遥测
|
||||||
const telemetryEntries = await Promise.all(
|
const telemetryEntries = await Promise.all(
|
||||||
@@ -271,6 +278,61 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 蚕匾 */}
|
||||||
|
<View className={styles.section}>
|
||||||
|
<Text className={styles.sectionTitle}>蚕匾</Text>
|
||||||
|
{trays.length > 0 ? (
|
||||||
|
<View className={styles.deviceList}>
|
||||||
|
{trays.map((t) => (
|
||||||
|
<View key={t.id} className={styles.deviceItem}>
|
||||||
|
<View className={styles.deviceLeft}>
|
||||||
|
<Text className={styles.deviceIcon}>🪵</Text>
|
||||||
|
<View className={styles.deviceInfo}>
|
||||||
|
<Text className={styles.deviceName}>{t.name}</Text>
|
||||||
|
<Text className={styles.deviceMeta}>
|
||||||
|
{[t.code, t.position].filter(Boolean).join(' · ') || '无位置信息'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className={styles.emptyWrap}>
|
||||||
|
<Text className={styles.emptyIcon}>📭</Text>
|
||||||
|
<Text className={styles.emptyText}>该蚕房暂无蚕匾</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 当前批次 */}
|
||||||
|
<View className={styles.section}>
|
||||||
|
<Text className={styles.sectionTitle}>当前批次</Text>
|
||||||
|
{batches.length > 0 ? (
|
||||||
|
<View className={styles.deviceList}>
|
||||||
|
{batches.map((b) => (
|
||||||
|
<View key={b.id} className={styles.deviceItem}>
|
||||||
|
<View className={styles.deviceLeft}>
|
||||||
|
<Text className={styles.deviceIcon}>🐛</Text>
|
||||||
|
<View className={styles.deviceInfo}>
|
||||||
|
<Text className={styles.deviceName}>{b.name}</Text>
|
||||||
|
<Text className={styles.deviceMeta}>
|
||||||
|
{batchStatusLabel(b.status)} · {b.instar ? `${b.instar}龄` : '龄期未设置'}
|
||||||
|
{b.variety ? ` · ${b.variety}` : ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className={styles.emptyWrap}>
|
||||||
|
<Text className={styles.emptyIcon}>📭</Text>
|
||||||
|
<Text className={styles.emptyText}>该蚕房暂无批次</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { get, post, patch, del } from '../api/http';
|
||||||
|
|
||||||
|
export interface Tray {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code?: string;
|
||||||
|
position?: string;
|
||||||
|
roomId: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Batch {
|
||||||
|
id: string;
|
||||||
|
roomId: string;
|
||||||
|
name: string;
|
||||||
|
variety?: string;
|
||||||
|
source?: string;
|
||||||
|
seedBatchNo?: string;
|
||||||
|
quarantineNo?: string;
|
||||||
|
instar?: number;
|
||||||
|
enteredAt?: string;
|
||||||
|
mountAt?: string;
|
||||||
|
status: string;
|
||||||
|
note?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RearingRecord {
|
||||||
|
id: string;
|
||||||
|
batchId: string;
|
||||||
|
recordDate: string;
|
||||||
|
instar?: number;
|
||||||
|
mulberrySource?: string;
|
||||||
|
density?: string;
|
||||||
|
note?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listTrays = (params?: any) => get<Tray[]>('/trays', { params });
|
||||||
|
export const createTray = (data: Partial<Tray>) => post<Tray>('/trays', data);
|
||||||
|
export const updateTray = (id: string, data: Partial<Tray>) => patch<Tray>(`/trays/${id}`, data);
|
||||||
|
export const deleteTray = (id: string) => del(`/trays/${id}`);
|
||||||
|
|
||||||
|
export const listBatches = (params?: any) => get<Batch[]>('/batches', { params });
|
||||||
|
export const createBatch = (data: Partial<Batch>) => post<Batch>('/batches', data);
|
||||||
|
export const updateBatch = (id: string, data: Partial<Batch>) => patch<Batch>(`/batches/${id}`, data);
|
||||||
|
export const deleteBatch = (id: string) => del(`/batches/${id}`);
|
||||||
|
|
||||||
|
export const listRearingRecords = (batchId: string) =>
|
||||||
|
get<RearingRecord[]>('/rearing-records', { params: { batchId } });
|
||||||
|
export const createRearingRecord = (data: Partial<RearingRecord>) =>
|
||||||
|
post<RearingRecord>('/rearing-records', data);
|
||||||
|
export const deleteRearingRecord = (id: string) => del(`/rearing-records/${id}`);
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
|
ProfileOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -32,6 +33,7 @@ const menuData = [
|
|||||||
{ path: '/videos', name: '视频监控', icon: <VideoCameraOutlined />, permission: 'video:read' },
|
{ path: '/videos', name: '视频监控', icon: <VideoCameraOutlined />, permission: 'video:read' },
|
||||||
{ path: '/logs', name: '控制日志', icon: <FileTextOutlined />, permission: 'log:read' },
|
{ path: '/logs', name: '控制日志', icon: <FileTextOutlined />, permission: 'log:read' },
|
||||||
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read' },
|
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read' },
|
||||||
|
{ path: '/batches', name: '批次管理', icon: <ProfileOutlined />, permission: 'batch:read' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 角色中文名
|
// 角色中文名
|
||||||
|
|||||||
@@ -0,0 +1,438 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button, DatePicker, Form, Input, InputNumber, Modal, Popconfirm, Select, Switch, Tabs, Tag, message,
|
||||||
|
} from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
listTrays,
|
||||||
|
createTray,
|
||||||
|
updateTray,
|
||||||
|
deleteTray,
|
||||||
|
listBatches,
|
||||||
|
createBatch,
|
||||||
|
updateBatch,
|
||||||
|
deleteBatch,
|
||||||
|
listRearingRecords,
|
||||||
|
createRearingRecord,
|
||||||
|
deleteRearingRecord,
|
||||||
|
type Tray,
|
||||||
|
type Batch,
|
||||||
|
type RearingRecord,
|
||||||
|
} from '../dal/trayBatch';
|
||||||
|
import { listHouses, type SilkwormHouse } from '../dal/silkworm';
|
||||||
|
import { authService } from '../services/auth';
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
rearing: '饲养中',
|
||||||
|
mounted: '已上蔟',
|
||||||
|
finished: '已结束',
|
||||||
|
};
|
||||||
|
|
||||||
|
const canWrite = () => authService.hasPermission('batch:write');
|
||||||
|
|
||||||
|
type BatchForm = Omit<Partial<Batch>, 'enteredAt' | 'mountAt'> & {
|
||||||
|
enteredAt?: dayjs.Dayjs;
|
||||||
|
mountAt?: dayjs.Dayjs;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RearingForm = Omit<Partial<RearingRecord>, 'recordDate'> & {
|
||||||
|
recordDate?: dayjs.Dayjs;
|
||||||
|
};
|
||||||
|
|
||||||
|
function BatchTab() {
|
||||||
|
const [rooms, setRooms] = useState<SilkwormHouse[]>([]);
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [form] = Form.useForm<BatchForm>();
|
||||||
|
const [editing, setEditing] = useState<Batch | null>(null);
|
||||||
|
const [recordOpen, setRecordOpen] = useState(false);
|
||||||
|
const [recordBatch, setRecordBatch] = useState<Batch | null>(null);
|
||||||
|
const [records, setRecords] = useState<RearingRecord[]>([]);
|
||||||
|
const [recordForm] = Form.useForm<RearingForm>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listHouses()
|
||||||
|
.then((r) => setRooms(r.items))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const roomEnum = Object.fromEntries(rooms.map((r) => [r.id, { text: r.name }]));
|
||||||
|
const roomName = (id: string) => rooms.find((r) => r.id === id)?.name || id;
|
||||||
|
|
||||||
|
const openRecords = async (b: Batch) => {
|
||||||
|
setRecordBatch(b);
|
||||||
|
const list = await listRearingRecords(b.id).catch(() => [] as RearingRecord[]);
|
||||||
|
setRecords(list);
|
||||||
|
recordForm.setFieldsValue({ recordDate: dayjs() });
|
||||||
|
setRecordOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ProColumns<Batch>[] = [
|
||||||
|
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 },
|
||||||
|
{ title: '名称', dataIndex: 'name' },
|
||||||
|
{
|
||||||
|
title: '蚕房',
|
||||||
|
dataIndex: 'roomId',
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: roomEnum,
|
||||||
|
render: (_, r) => roomName(r.roomId),
|
||||||
|
},
|
||||||
|
{ title: '品种', dataIndex: 'variety', search: false, render: (_, r) => r.variety || '-' },
|
||||||
|
{ title: '龄期', dataIndex: 'instar', search: false, width: 70, render: (_, r) => (r.instar ? `${r.instar}龄` : '-') },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
search: false,
|
||||||
|
render: (_, r) => <Tag color={r.status === 'rearing' ? 'green' : 'default'}>{STATUS_LABELS[r.status] || r.status}</Tag>,
|
||||||
|
},
|
||||||
|
{ title: '入房时间', dataIndex: 'enteredAt', search: false, render: (_, r) => (r.enteredAt ? dayjs(r.enteredAt).format('YYYY-MM-DD') : '-') },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, r) => [
|
||||||
|
<a key="records" onClick={() => openRecords(r)}>
|
||||||
|
饲养记录
|
||||||
|
</a>,
|
||||||
|
<a
|
||||||
|
key="edit"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(r);
|
||||||
|
form.setFieldsValue({
|
||||||
|
...r,
|
||||||
|
enteredAt: r.enteredAt ? dayjs(r.enteredAt) : undefined,
|
||||||
|
mountAt: r.mountAt ? dayjs(r.mountAt) : undefined,
|
||||||
|
});
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a>,
|
||||||
|
...(canWrite()
|
||||||
|
? [
|
||||||
|
<Popconfirm
|
||||||
|
key="del"
|
||||||
|
title="确认删除该批次?其饲养记录将一并删除。"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteBatch(r.id);
|
||||||
|
message.success('已删除');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditing(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ status: 'rearing' });
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable<Batch>
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
search={{ labelWidth: 'auto' }}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listBatches({ roomId: params.roomId, status: params.status });
|
||||||
|
return { data: res, total: res.length, success: true };
|
||||||
|
}}
|
||||||
|
toolBarRender={() =>
|
||||||
|
canWrite()
|
||||||
|
? [
|
||||||
|
<Button key="new" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
新建批次
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
title={editing ? '编辑批次' : '新建批次'}
|
||||||
|
open={modalOpen}
|
||||||
|
width={680}
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
onOk={async () => {
|
||||||
|
const v = await form.validateFields();
|
||||||
|
const payload = {
|
||||||
|
...v,
|
||||||
|
enteredAt: v.enteredAt ? v.enteredAt.toISOString() : undefined,
|
||||||
|
mountAt: v.mountAt ? v.mountAt.toISOString() : undefined,
|
||||||
|
};
|
||||||
|
if (editing) await updateBatch(editing.id, payload);
|
||||||
|
else await createBatch(payload);
|
||||||
|
message.success('保存成功');
|
||||||
|
setModalOpen(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
|
||||||
|
<Input placeholder="如:2026春-1批" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="蚕房" name="roomId" rules={[{ required: true, message: '请选择蚕房' }]}>
|
||||||
|
<Select options={rooms.map((r) => ({ value: r.id, label: r.name }))} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="品种" name="variety">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="蚕种来源" name="source">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="蚕种批次号" name="seedBatchNo">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="检疫证明编号" name="quarantineNo">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="龄期(1-5)" name="instar">
|
||||||
|
<InputNumber min={1} max={5} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="入房时间" name="enteredAt">
|
||||||
|
<DatePicker style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="上蔟时间" name="mountAt">
|
||||||
|
<DatePicker style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="状态" name="status">
|
||||||
|
<Select options={Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注" name="note">
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={recordBatch ? `饲养记录 - ${recordBatch.name}` : '饲养记录'}
|
||||||
|
open={recordOpen}
|
||||||
|
width={720}
|
||||||
|
onCancel={() => setRecordOpen(false)}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<Form form={recordForm} layout="inline" style={{ marginBottom: 16 }}>
|
||||||
|
<Form.Item label="日期" name="recordDate">
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="龄期" name="instar">
|
||||||
|
<InputNumber min={1} max={5} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="桑叶来源" name="mulberrySource">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="密度" name="density">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注" name="note">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!recordBatch) return;
|
||||||
|
const v = await recordForm.validateFields();
|
||||||
|
await createRearingRecord({
|
||||||
|
batchId: recordBatch.id,
|
||||||
|
...v,
|
||||||
|
recordDate: v.recordDate ? v.recordDate.toISOString() : new Date().toISOString(),
|
||||||
|
});
|
||||||
|
message.success('已添加');
|
||||||
|
recordForm.setFieldsValue({ recordDate: dayjs(), instar: undefined, mulberrySource: undefined, density: undefined, note: undefined });
|
||||||
|
setRecords(await listRearingRecords(recordBatch.id));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<ProTable<RearingRecord>
|
||||||
|
rowKey="id"
|
||||||
|
search={false}
|
||||||
|
options={false}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={records}
|
||||||
|
columns={[
|
||||||
|
{ title: '日期', dataIndex: 'recordDate', render: (_, r) => dayjs(r.recordDate).format('YYYY-MM-DD') },
|
||||||
|
{ title: '龄期', dataIndex: 'instar', render: (_, r) => (r.instar ? `${r.instar}龄` : '-') },
|
||||||
|
{ title: '桑叶来源', dataIndex: 'mulberrySource', render: (_, r) => r.mulberrySource || '-' },
|
||||||
|
{ title: '密度', dataIndex: 'density', render: (_, r) => r.density || '-' },
|
||||||
|
{ title: '备注', dataIndex: 'note', render: (_, r) => r.note || '-' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, r) => [
|
||||||
|
<Popconfirm
|
||||||
|
key="del"
|
||||||
|
title="确认删除该记录?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteRearingRecord(r.id);
|
||||||
|
message.success('已删除');
|
||||||
|
if (recordBatch) setRecords(await listRearingRecords(recordBatch.id));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrayTab() {
|
||||||
|
const [rooms, setRooms] = useState<SilkwormHouse[]>([]);
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [form] = Form.useForm<Partial<Tray>>();
|
||||||
|
const [editing, setEditing] = useState<Tray | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listHouses()
|
||||||
|
.then((r) => setRooms(r.items))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const roomEnum = Object.fromEntries(rooms.map((r) => [r.id, { text: r.name }]));
|
||||||
|
const roomName = (id: string) => rooms.find((r) => r.id === id)?.name || id;
|
||||||
|
|
||||||
|
const columns: ProColumns<Tray>[] = [
|
||||||
|
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 },
|
||||||
|
{ title: '名称', dataIndex: 'name' },
|
||||||
|
{ title: '编号', dataIndex: 'code', search: false, render: (_, r) => r.code || '-' },
|
||||||
|
{ title: '位置', dataIndex: 'position', search: false, render: (_, r) => r.position || '-' },
|
||||||
|
{
|
||||||
|
title: '蚕房',
|
||||||
|
dataIndex: 'roomId',
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: roomEnum,
|
||||||
|
render: (_, r) => roomName(r.roomId),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '启用',
|
||||||
|
dataIndex: 'enabled',
|
||||||
|
search: false,
|
||||||
|
width: 80,
|
||||||
|
render: (_, r) => (r.enabled === false ? <Tag>停用</Tag> : <Tag color="green">启用</Tag>),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, r) => [
|
||||||
|
<a
|
||||||
|
key="edit"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(r);
|
||||||
|
form.setFieldsValue(r);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a>,
|
||||||
|
...(canWrite()
|
||||||
|
? [
|
||||||
|
<Popconfirm
|
||||||
|
key="del"
|
||||||
|
title="确认删除该蚕匾?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteTray(r.id);
|
||||||
|
message.success('已删除');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a style={{ color: '#ff4d4f' }}>删除</a>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable<Tray>
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
search={{ labelWidth: 'auto' }}
|
||||||
|
request={async (params) => {
|
||||||
|
const res = await listTrays({ roomId: params.roomId });
|
||||||
|
return { data: res, total: res.length, success: true };
|
||||||
|
}}
|
||||||
|
toolBarRender={() =>
|
||||||
|
canWrite()
|
||||||
|
? [
|
||||||
|
<Button
|
||||||
|
key="new"
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ enabled: true });
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新建蚕匾
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
title={editing ? '编辑蚕匾' : '新建蚕匾'}
|
||||||
|
open={modalOpen}
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
onOk={async () => {
|
||||||
|
const v = await form.validateFields();
|
||||||
|
if (editing) await updateTray(editing.id, v);
|
||||||
|
else await createTray(v);
|
||||||
|
message.success('保存成功');
|
||||||
|
setModalOpen(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
|
||||||
|
<Input placeholder="如:1号匾" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="编号" name="code">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="位置" name="position">
|
||||||
|
<Input placeholder="如:东侧第 2 排" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="蚕房" name="roomId" rules={[{ required: true, message: '请选择蚕房' }]}>
|
||||||
|
<Select options={rooms.map((r) => ({ value: r.id, label: r.name }))} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BatchesPage() {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="batches"
|
||||||
|
items={[
|
||||||
|
{ key: 'batches', label: '批次管理', children: <BatchTab /> },
|
||||||
|
{ key: 'trays', label: '蚕匾管理', children: <TrayTab /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import Alert from './pages/Alert';
|
|||||||
import Video from './pages/Video';
|
import Video from './pages/Video';
|
||||||
import Log from './pages/Log';
|
import Log from './pages/Log';
|
||||||
import Knowledge from './pages/Knowledge';
|
import Knowledge from './pages/Knowledge';
|
||||||
|
import Batches from './pages/Batches';
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from './pages/NotFound';
|
||||||
import { BasicLayout } from './layout/BasicLayout';
|
import { BasicLayout } from './layout/BasicLayout';
|
||||||
import { authService } from './services/auth';
|
import { authService } from './services/auth';
|
||||||
@@ -48,6 +49,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'videos', element: <RequirePermission permission="video:read"><Video /></RequirePermission> },
|
{ path: 'videos', element: <RequirePermission permission="video:read"><Video /></RequirePermission> },
|
||||||
{ path: 'logs', element: <RequirePermission permission="log:read"><Log /></RequirePermission> },
|
{ path: 'logs', element: <RequirePermission permission="log:read"><Log /></RequirePermission> },
|
||||||
{ path: 'knowledge', element: <RequirePermission permission="knowledge:read"><Knowledge /></RequirePermission> },
|
{ path: 'knowledge', element: <RequirePermission permission="knowledge:read"><Knowledge /></RequirePermission> },
|
||||||
|
{ path: 'batches', element: <RequirePermission permission="batch:read"><Batches /></RequirePermission> },
|
||||||
{ path: '404', element: <NotFound /> },
|
{ path: '404', element: <NotFound /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user