From 42ff238e3d41b6bed32e8dcbc32e6171eae2f426 Mon Sep 17 00:00:00 2001 From: weijuesen Date: Wed, 12 Aug 2026 18:59:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(web/server-go):=20=E6=8A=80=E6=9C=AF?= =?UTF-8?q?=E5=91=98=E5=B7=A1=E6=A3=80=E8=AE=B0=E5=BD=95=E9=A1=B5=EF=BC=88?= =?UTF-8?q?#17=EF=BC=8C=E5=88=97=E8=A1=A8+=E8=AF=A6=E6=83=85+roomName?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-go/internal/handler/inspection.go | 14 +++ server-go/internal/model/inspection.go | 1 + web/src/dal/inspections.ts | 23 ++++ web/src/layout/BasicLayout.tsx | 2 + web/src/pages/Inspections.tsx | 130 +++++++++++++++++++++++ web/src/router.tsx | 2 + 6 files changed, 172 insertions(+) create mode 100644 web/src/dal/inspections.ts create mode 100644 web/src/pages/Inspections.tsx diff --git a/server-go/internal/handler/inspection.go b/server-go/internal/handler/inspection.go index 8f8a526..f89e0c8 100644 --- a/server-go/internal/handler/inspection.go +++ b/server-go/internal/handler/inspection.go @@ -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) } } diff --git a/server-go/internal/model/inspection.go b/server-go/internal/model/inspection.go index 42a47c3..936708d 100644 --- a/server-go/internal/model/inspection.go +++ b/server-go/internal/model/inspection.go @@ -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" } diff --git a/web/src/dal/inspections.ts b/web/src/dal/inspections.ts new file mode 100644 index 0000000..c2064fd --- /dev/null +++ b/web/src/dal/inspections.ts @@ -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('/inspections', { params }); diff --git a/web/src/layout/BasicLayout.tsx b/web/src/layout/BasicLayout.tsx index a894570..77e3157 100644 --- a/web/src/layout/BasicLayout.tsx +++ b/web/src/layout/BasicLayout.tsx @@ -11,6 +11,7 @@ import { ProfileOutlined, ExperimentOutlined, ShoppingOutlined, + CameraOutlined, SettingOutlined, LogoutOutlined, UserOutlined, @@ -38,6 +39,7 @@ const menuData = [ { path: '/batches', name: '批次管理', icon: , permission: 'batch:read' }, { path: '/lamp-tests', name: 'LAMP 检测', icon: , permission: 'lamp:read' }, { path: '/consumables', name: '耗材管理', icon: , permission: 'consumable:read' }, + { path: '/inspections', name: '巡检记录', icon: , permission: 'inspection:read' }, ]; // 角色中文名 diff --git a/web/src/pages/Inspections.tsx b/web/src/pages/Inspections.tsx new file mode 100644 index 0000000..54ff5ce --- /dev/null +++ b/web/src/pages/Inspections.tsx @@ -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 = { + healthy: '健康', + sick: '疑似异常', +}; + +const RISK_LABELS: Record = { + 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(null); + + const columns: ProColumns[] = [ + { 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 ? ( + {CLASS_LABELS[c]} + ) : ( + '-' + ); + }, + }, + { + title: '风险等级', + dataIndex: 'riskLevel', + search: false, + render: (_, r) => { + const l = RISK_LABELS[r.riskLevel || ''] || RISK_LABELS.green; + return r.riskScore !== undefined ? ( + + {l.text}({Math.round(r.riskScore!)} 分) + + ) : ( + '-' + ); + }, + }, + { + title: '图片', + dataIndex: 'imageUrl', + search: false, + width: 80, + render: (_, r) => (r.imageUrl ? : '-'), + }, + { title: '状态', dataIndex: 'aiStatus', search: false, render: (_, r) => (r.aiStatus === 'done' ? 成功 : 失败) }, + { + title: '操作', + valueType: 'option', + render: (_, r) => [ + setDetail(r)}> + 详情 + , + ], + }, + ]; + + return ( + <> + + rowKey="id" + columns={columns} + search={false} + request={async () => { + const res = await listInspections({ limit: 100 }); + return { data: res, total: res.length, success: true }; + }} + /> + setDetail(null)} + > + {detail ? ( +
+ {detail.imageUrl ? ( + + ) : null} + + 时间:{detail.createdAt ? dayjs(detail.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'} +
+ 蚕房:{detail.roomName || detail.roomId || '-'} +
+ 状态:{detail.aiStatus === 'done' ? '检测成功' : '检测失败'} +
+ 风险分:{detail.riskScore !== undefined ? Math.round(detail.riskScore) : '-'}( + {detail.riskLevel ? RISK_LABELS[detail.riskLevel]?.text || detail.riskLevel : '-'}) +
+ 检测结果 + {detail.detections && detail.detections.length > 0 ? ( + detail.detections.map((d: AIDetection, i: number) => ( +
+ {CLASS_LABELS[d.class] || d.class} + 置信度 {(d.confidence * 100).toFixed(1)}% + + 框 x:{Math.round(d.bbox.x)} y:{Math.round(d.bbox.y)} w:{Math.round(d.bbox.w)} h:{Math.round(d.bbox.h)} + +
+ )) + ) : ( + 无检测结果 + )} +
+ ) : null} +
+ + ); +} diff --git a/web/src/router.tsx b/web/src/router.tsx index 5e12867..b54aaf3 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -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: }, { path: 'lamp-tests', element: }, { path: 'consumables', element: }, + { path: 'inspections', element: }, { path: '404', element: }, ], },