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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type InspectionRecord struct {
|
||||
IdempotencyKey *string `gorm:"column:idempotency_key;size:128;uniqueIndex" json:"idempotencyKey,omitempty"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
|
||||
RoomName *string `gorm:"-" json:"roomName,omitempty"`
|
||||
}
|
||||
|
||||
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,
|
||||
ExperimentOutlined,
|
||||
ShoppingOutlined,
|
||||
CameraOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
@@ -38,6 +39,7 @@ const menuData = [
|
||||
{ path: '/batches', name: '批次管理', icon: <ProfileOutlined />, permission: 'batch:read' },
|
||||
{ path: '/lamp-tests', name: 'LAMP 检测', icon: <ExperimentOutlined />, permission: 'lamp: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 LampTests from './pages/LampTests';
|
||||
import Consumables from './pages/Consumables';
|
||||
import Inspections from './pages/Inspections';
|
||||
import NotFound from './pages/NotFound';
|
||||
import { BasicLayout } from './layout/BasicLayout';
|
||||
import { authService } from './services/auth';
|
||||
@@ -54,6 +55,7 @@ export const router = createBrowserRouter([
|
||||
{ path: 'batches', element: <RequirePermission permission="batch:read"><Batches /></RequirePermission> },
|
||||
{ path: 'lamp-tests', element: <RequirePermission permission="lamp:read"><LampTests /></RequirePermission> },
|
||||
{ path: 'consumables', element: <RequirePermission permission="consumable:read"><Consumables /></RequirePermission> },
|
||||
{ path: 'inspections', element: <RequirePermission permission="inspection:read"><Inspections /></RequirePermission> },
|
||||
{ path: '404', element: <NotFound /> },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user