feat(web/server-go): 技术员巡检记录页(#17,列表+详情+roomName)
This commit is contained in:
@@ -226,6 +226,20 @@ func listInspections(db *gorm.DB) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
var list []model.InspectionRecord
|
var list []model.InspectionRecord
|
||||||
q.Order("created_at DESC").Limit(limit).Find(&list)
|
q.Order("created_at DESC").Limit(limit).Find(&list)
|
||||||
|
// 联查蚕房名(非持久化字段)
|
||||||
|
var rooms []model.Room
|
||||||
|
db.Select("id", "name").Find(&rooms)
|
||||||
|
roomNames := make(map[string]string, len(rooms))
|
||||||
|
for _, r := range rooms {
|
||||||
|
roomNames[r.ID] = r.Name
|
||||||
|
}
|
||||||
|
for i := range list {
|
||||||
|
if list[i].RoomID != nil {
|
||||||
|
if n, ok := roomNames[*list[i].RoomID]; ok {
|
||||||
|
list[i].RoomName = &n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, list)
|
c.JSON(http.StatusOK, list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type InspectionRecord struct {
|
|||||||
IdempotencyKey *string `gorm:"column:idempotency_key;size:128;uniqueIndex" json:"idempotencyKey,omitempty"`
|
IdempotencyKey *string `gorm:"column:idempotency_key;size:128;uniqueIndex" json:"idempotencyKey,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
|
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
|
||||||
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
|
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
|
||||||
|
RoomName *string `gorm:"-" json:"roomName,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (InspectionRecord) TableName() string { return "inspection_records" }
|
func (InspectionRecord) TableName() string { return "inspection_records" }
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { get } from '../api/http';
|
||||||
|
|
||||||
|
export interface AIDetection {
|
||||||
|
bbox: { x: number; y: number; w: number; h: number };
|
||||||
|
class: string;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectionRecord {
|
||||||
|
id: string;
|
||||||
|
userId?: string;
|
||||||
|
roomId?: string;
|
||||||
|
roomName?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
detections?: AIDetection[];
|
||||||
|
riskScore?: number;
|
||||||
|
riskLevel?: string;
|
||||||
|
aiStatus: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listInspections = (params?: any) =>
|
||||||
|
get<InspectionRecord[]>('/inspections', { params });
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ProfileOutlined,
|
ProfileOutlined,
|
||||||
ExperimentOutlined,
|
ExperimentOutlined,
|
||||||
ShoppingOutlined,
|
ShoppingOutlined,
|
||||||
|
CameraOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -38,6 +39,7 @@ const menuData = [
|
|||||||
{ path: '/batches', name: '批次管理', icon: <ProfileOutlined />, permission: 'batch:read' },
|
{ path: '/batches', name: '批次管理', icon: <ProfileOutlined />, permission: 'batch:read' },
|
||||||
{ path: '/lamp-tests', name: 'LAMP 检测', icon: <ExperimentOutlined />, permission: 'lamp:read' },
|
{ path: '/lamp-tests', name: 'LAMP 检测', icon: <ExperimentOutlined />, permission: 'lamp:read' },
|
||||||
{ path: '/consumables', name: '耗材管理', icon: <ShoppingOutlined />, permission: 'consumable:read' },
|
{ path: '/consumables', name: '耗材管理', icon: <ShoppingOutlined />, permission: 'consumable:read' },
|
||||||
|
{ path: '/inspections', name: '巡检记录', icon: <CameraOutlined />, permission: 'inspection:read' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 角色中文名
|
// 角色中文名
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Drawer, Image, Tag, Typography } from 'antd';
|
||||||
|
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { listInspections, type AIDetection, type InspectionRecord } from '../dal/inspections';
|
||||||
|
|
||||||
|
const CLASS_LABELS: Record<string, string> = {
|
||||||
|
healthy: '健康',
|
||||||
|
sick: '疑似异常',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RISK_LABELS: Record<string, { color: string; text: string }> = {
|
||||||
|
green: { color: 'green', text: '绿' },
|
||||||
|
yellow: { color: 'gold', text: '黄' },
|
||||||
|
orange: { color: 'orange', text: '橙' },
|
||||||
|
red: { color: 'red', text: '红' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiClassOf = (rec: InspectionRecord) =>
|
||||||
|
rec.detections && rec.detections.length > 0
|
||||||
|
? rec.detections.some((d) => d.class !== 'healthy')
|
||||||
|
? 'sick'
|
||||||
|
: 'healthy'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
export default function InspectionsPage() {
|
||||||
|
const [detail, setDetail] = useState<InspectionRecord | null>(null);
|
||||||
|
|
||||||
|
const columns: ProColumns<InspectionRecord>[] = [
|
||||||
|
{ title: '序号', valueType: 'indexBorder', search: false, width: 60 },
|
||||||
|
{ title: '时间', dataIndex: 'createdAt', search: false, render: (_, r) => (r.createdAt ? dayjs(r.createdAt).format('YYYY-MM-DD HH:mm') : '-') },
|
||||||
|
{ title: '蚕房', dataIndex: 'roomName', search: false, render: (_, r) => r.roomName || r.roomId || '-' },
|
||||||
|
{
|
||||||
|
title: 'AI 结论',
|
||||||
|
search: false,
|
||||||
|
render: (_, r) => {
|
||||||
|
const c = aiClassOf(r);
|
||||||
|
return c ? (
|
||||||
|
<Tag color={c === 'sick' ? 'red' : 'green'}>{CLASS_LABELS[c]}</Tag>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '风险等级',
|
||||||
|
dataIndex: 'riskLevel',
|
||||||
|
search: false,
|
||||||
|
render: (_, r) => {
|
||||||
|
const l = RISK_LABELS[r.riskLevel || ''] || RISK_LABELS.green;
|
||||||
|
return r.riskScore !== undefined ? (
|
||||||
|
<Tag color={l.color}>
|
||||||
|
{l.text}({Math.round(r.riskScore!)} 分)
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '图片',
|
||||||
|
dataIndex: 'imageUrl',
|
||||||
|
search: false,
|
||||||
|
width: 80,
|
||||||
|
render: (_, r) => (r.imageUrl ? <Image src={r.imageUrl} width={48} height={48} style={{ objectFit: 'cover', borderRadius: 4 }} /> : '-'),
|
||||||
|
},
|
||||||
|
{ title: '状态', dataIndex: 'aiStatus', search: false, render: (_, r) => (r.aiStatus === 'done' ? <Tag color="green">成功</Tag> : <Tag color="red">失败</Tag>) },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, r) => [
|
||||||
|
<a key="view" onClick={() => setDetail(r)}>
|
||||||
|
详情
|
||||||
|
</a>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable<InspectionRecord>
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
search={false}
|
||||||
|
request={async () => {
|
||||||
|
const res = await listInspections({ limit: 100 });
|
||||||
|
return { data: res, total: res.length, success: true };
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Drawer
|
||||||
|
title="巡检详情"
|
||||||
|
open={!!detail}
|
||||||
|
width={560}
|
||||||
|
onClose={() => setDetail(null)}
|
||||||
|
>
|
||||||
|
{detail ? (
|
||||||
|
<div>
|
||||||
|
{detail.imageUrl ? (
|
||||||
|
<Image src={detail.imageUrl} style={{ width: '100%', borderRadius: 8, marginBottom: 16 }} />
|
||||||
|
) : null}
|
||||||
|
<Typography.Paragraph>
|
||||||
|
时间:{detail.createdAt ? dayjs(detail.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
<br />
|
||||||
|
蚕房:{detail.roomName || detail.roomId || '-'}
|
||||||
|
<br />
|
||||||
|
状态:{detail.aiStatus === 'done' ? '检测成功' : '检测失败'}
|
||||||
|
<br />
|
||||||
|
风险分:{detail.riskScore !== undefined ? Math.round(detail.riskScore) : '-'}(
|
||||||
|
{detail.riskLevel ? RISK_LABELS[detail.riskLevel]?.text || detail.riskLevel : '-'})
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Typography.Title level={5}>检测结果</Typography.Title>
|
||||||
|
{detail.detections && detail.detections.length > 0 ? (
|
||||||
|
detail.detections.map((d: AIDetection, i: number) => (
|
||||||
|
<div key={i} style={{ padding: '6px 0', borderBottom: '1px solid #f0f0f0' }}>
|
||||||
|
<Tag color={d.class === 'healthy' ? 'green' : 'red'}>{CLASS_LABELS[d.class] || d.class}</Tag>
|
||||||
|
<span>置信度 {(d.confidence * 100).toFixed(1)}%</span>
|
||||||
|
<span style={{ marginLeft: 12, color: '#999' }}>
|
||||||
|
框 x:{Math.round(d.bbox.x)} y:{Math.round(d.bbox.y)} w:{Math.round(d.bbox.w)} h:{Math.round(d.bbox.h)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">无检测结果</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import Knowledge from './pages/Knowledge';
|
|||||||
import Batches from './pages/Batches';
|
import Batches from './pages/Batches';
|
||||||
import LampTests from './pages/LampTests';
|
import LampTests from './pages/LampTests';
|
||||||
import Consumables from './pages/Consumables';
|
import Consumables from './pages/Consumables';
|
||||||
|
import Inspections from './pages/Inspections';
|
||||||
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';
|
||||||
@@ -54,6 +55,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'batches', element: <RequirePermission permission="batch:read"><Batches /></RequirePermission> },
|
{ path: 'batches', element: <RequirePermission permission="batch:read"><Batches /></RequirePermission> },
|
||||||
{ path: 'lamp-tests', element: <RequirePermission permission="lamp:read"><LampTests /></RequirePermission> },
|
{ path: 'lamp-tests', element: <RequirePermission permission="lamp:read"><LampTests /></RequirePermission> },
|
||||||
{ path: 'consumables', element: <RequirePermission permission="consumable:read"><Consumables /></RequirePermission> },
|
{ path: 'consumables', element: <RequirePermission permission="consumable:read"><Consumables /></RequirePermission> },
|
||||||
|
{ path: 'inspections', element: <RequirePermission permission="inspection:read"><Inspections /></RequirePermission> },
|
||||||
{ path: '404', element: <NotFound /> },
|
{ path: '404', element: <NotFound /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user