feat(web/server-go): 技术员巡检记录页(#17,列表+详情+roomName)

This commit is contained in:
weijuesen
2026-08-12 18:59:18 +08:00
parent d8c4b436df
commit 42ff238e3d
6 changed files with 172 additions and 0 deletions
+14
View File
@@ -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)
}
}
+1
View File
@@ -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" }
+23
View File
@@ -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 });
+2
View File
@@ -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' },
];
// 角色中文名
+130
View File
@@ -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>
</>
);
}
+2
View File
@@ -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 /> },
],
},