From 4092854b0bea9e1fc02f29222c6f7cd876e565a5 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 17 Aug 2026 09:43:20 +0800
Subject: [PATCH] feat:项目基础整理
---
apps/workbench-console/src/components/ObjectDetectionPanel.vue | 51 +++++++++++++++++++++++++++------------------------
1 files changed, 27 insertions(+), 24 deletions(-)
diff --git a/apps/workbench-console/src/components/ObjectDetectionPanel.vue b/apps/workbench-console/src/components/ObjectDetectionPanel.vue
index 6959579..a9ba213 100644
--- a/apps/workbench-console/src/components/ObjectDetectionPanel.vue
+++ b/apps/workbench-console/src/components/ObjectDetectionPanel.vue
@@ -1,37 +1,40 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
-import { FileImageOutlined, InfoCircleOutlined } from "@ant-design/icons-vue";
-
-import { artifactUrl } from "@/api/artifacts";
+import { FileImageOutlined, InfoCircleOutlined, PlayCircleOutlined, UploadOutlined } from "@ant-design/icons-vue";
+import { createDetectionRun, artifactUrl, readFileAsPayload } from "@/api/artifacts";
import ArtifactState from "@/components/ArtifactState.vue";
import { useArtifactStore } from "@/stores/artifacts";
const store = useArtifactStore();
-const mode = ref<"annotated" | "original">("annotated");
+const caseId = ref("");
const selectedName = ref("");
-const selectedImage = computed(() => store.detectionImages.find((item) => item.file === selectedName.value) ?? store.detectionImages[0]);
-const previewSource = computed(() => {
- if (!selectedImage.value) return "";
- const path = mode.value === "annotated"
- ? `shared/outputs/01-object-detection/${selectedImage.value.annotated_file}`
- : `shared/data/raw/01-object-detection/${selectedImage.value.file}`;
- return artifactUrl(path);
-});
-
-onMounted(async () => {
- await store.loadDetection();
- const mostDetections = [...store.detectionImages].sort((left, right) => right.detections.length - left.detections.length)[0];
- selectedName.value = mostDetections?.file ?? "";
-});
+const showRunForm = ref(false);
+const files = ref<File[]>([]);
+const running = ref(false);
+const runError = ref<string | null>(null);
+const searchText = ref("");
+const currentCase = computed(() => store.detectionCases[caseId.value] ?? Object.values(store.detectionCases)[0]);
+const selectedImage = computed(() => currentCase.value?.images.find((item) => item.file === selectedName.value) ?? currentCase.value?.images[0]);
+const caseOptions = computed(() => Object.values(store.detectionCases).map((item) => ({ value: item.id, label: item.label })));
+const filteredCaseOptions = computed(() => caseOptions.value.filter((item) => item.label.toLowerCase().includes(searchText.value.trim().toLowerCase())));
+function syncSelection() { const candidate = currentCase.value?.images.reduce((best, item) => item.detections.length > (best?.detections.length ?? -1) ? item : best, undefined as typeof currentCase.value.images[number] | undefined); selectedName.value = candidate?.file ?? ""; }
+function beforeUpload(file: File) { files.value = [...files.value, file]; return false; }
+function removeFile(file: { name: string }) { files.value = files.value.filter((item) => item.name !== file.name); }
+async function submitRun() { if (!files.value.length) { runError.value = "请至少选择一张 JPG、JPEG 或 PNG 图像。"; return; } running.value = true; runError.value = null; try { const images = await Promise.all(files.value.map(readFileAsPayload)); const { run } = await createDetectionRun(images); await store.loadDetection(true); caseId.value = run.id; syncSelection(); files.value = []; showRunForm.value = false; } catch (error) { runError.value = error instanceof Error ? error.message : "目标检测运行失败"; } finally { running.value = false; } }
+onMounted(async () => { await store.loadDetection(); caseId.value = Object.keys(store.detectionCases)[0] ?? ""; syncSelection(); });
</script>
<template>
<ArtifactState :loading="store.loading" :error="store.error" />
- <template v-if="store.detectionRun && selectedImage">
- <a-row :gutter="[18, 18]">
- <a-col :xs="24" :xl="17"><section class="surface-section"><div class="section-toolbar"><a-select v-model:value="selectedName" :options="store.detectionImages.map((item) => ({ value: item.file, label: `${item.file} · ${item.detections.length} 个候选` }))" /><a-segmented v-model:value="mode" :options="[{ label: '标注结果', value: 'annotated' }, { label: '原始影像', value: 'original' }]" /></div><a-image class="detection-image" :src="previewSource" :alt="`${selectedImage.file} 预览`" /><p class="image-caption">{{ selectedImage.width }} x {{ selectedImage.height }} 像素 · {{ selectedImage.detections.length }} 个候选</p></section></a-col>
- <a-col :xs="24" :xl="7"><section class="surface-section"><h2>本次运行</h2><a-descriptions size="small" :column="1"><a-descriptions-item label="处理影像">{{ store.detectionRun.processed_images }} 张</a-descriptions-item><a-descriptions-item label="检测候选">{{ store.detectionRun.detection_count }} 个</a-descriptions-item><a-descriptions-item label="耗时">{{ store.detectionRun.elapsed_seconds }} 秒</a-descriptions-item><a-descriptions-item label="设备">{{ store.detectionRun.device }}</a-descriptions-item><a-descriptions-item label="模型">{{ store.detectionRun.model }}</a-descriptions-item><a-descriptions-item label="阈值">{{ store.detectionRun.confidence }}</a-descriptions-item></a-descriptions><a-divider /><a-space direction="vertical"><a-button type="link" :href="artifactUrl('shared/outputs/01-object-detection/detections.json')" target="_blank"><FileImageOutlined />检测 JSON</a-button><a-button type="link" :href="artifactUrl('shared/outputs/01-object-detection/run_metadata.json')" target="_blank"><InfoCircleOutlined />运行元数据</a-button></a-space></section></a-col>
- </a-row>
- <section class="surface-section"><div class="section-heading"><div><h2>当前影像检测明细</h2><p>边界框是 JPEG 像素坐标,不含可用地理坐标。</p></div></div><a-table :data-source="selectedImage.detections" :pagination="false" row-key="bbox_xyxy" size="small" :scroll="{ x: 680 }"><a-table-column title="类别" data-index="class_name" key="class_name" /><a-table-column title="置信度" key="confidence" align="right"><template #default="{ record }">{{ (record.confidence * 100).toFixed(1) }}%</template></a-table-column><a-table-column title="像素框 x1, y1, x2, y2" key="bbox"><template #default="{ record }">{{ record.bbox_xyxy.map((value: number) => value.toFixed(1)).join(', ') }}</template></a-table-column><template #emptyText>该影像没有达到当前阈值的候选目标。</template></a-table></section>
+ <section class="workspace-command">
+ <div><h2>新建检测运行</h2><p>上传少量代表性影像,CPU 基线会生成独立结果,不覆盖既有案例。</p></div>
+ <a-button type="primary" @click="showRunForm = !showRunForm"><PlayCircleOutlined />{{ showRunForm ? "收起运行表单" : "上传并运行" }}</a-button>
+ </section>
+ <section v-if="showRunForm" class="surface-section run-form"><a-alert type="info" show-icon message="单次最多 12 张图像;当前基线识别人员和常见车辆,树木不在有效类别内。" /><a-upload class="run-upload" multiple accept=".jpg,.jpeg,.png" :file-list="files.map((file) => ({ uid: file.name, name: file.name, status: 'done' as const }))" :before-upload="beforeUpload" @remove="removeFile"><a-button><UploadOutlined />选择图像</a-button></a-upload><a-alert v-if="runError" type="error" show-icon :message="runError" /><a-button type="primary" :loading="running" :disabled="!files.length" @click="submitRun"><PlayCircleOutlined />开始检测</a-button></section>
+ <template v-if="currentCase && selectedImage">
+ <div class="detection-workspace">
+ <a-row :gutter="[18, 18]"><a-col :xs="24" :xl="5"><section class="surface-section run-library"><h2>案例库</h2><a-input-search v-model:value="searchText" placeholder="搜索运行" allow-clear /><a-list size="small" :data-source="filteredCaseOptions"><template #renderItem="{ item }"><a-list-item class="run-item" :class="{ active: item.value === currentCase.id }" @click="caseId = item.value; syncSelection()">{{ item.label }}</a-list-item></template></a-list></section></a-col><a-col :xs="24" :xl="19"><section class="surface-section"><div class="section-toolbar"><a-select v-model:value="selectedName" :options="currentCase.images.map((item) => ({ value: item.file, label: `${item.file} · ${item.detections.length} 个候选` }))" /><span class="toolbar-note">{{ currentCase.note }}</span></div><div class="comparison-grid"><figure><figcaption>原始影像</figcaption><a-image class="detection-image" :src="artifactUrl(`${currentCase.inputRoot}/${selectedImage.file}`)" :alt="`${selectedImage.file} 原始影像`" /></figure><figure><figcaption>标注结果</figcaption><a-image class="detection-image" :src="artifactUrl(`${currentCase.artifactRoot}/${selectedImage.annotated_file}`)" :alt="`${selectedImage.file} 标注结果`" /></figure></div><p class="image-caption">{{ selectedImage.width }} x {{ selectedImage.height }} 像素 · {{ selectedImage.detections.length }} 个候选</p></section></a-col></a-row>
+ <a-row :gutter="[18, 18]"><a-col :xs="24" :xl="8"><section class="surface-section"><h2>本次运行</h2><a-descriptions size="small" :column="1"><a-descriptions-item label="处理影像">{{ currentCase.run.processed_images }} 张</a-descriptions-item><a-descriptions-item label="检测候选">{{ currentCase.run.detection_count }} 个</a-descriptions-item><a-descriptions-item label="耗时">{{ currentCase.run.elapsed_seconds }} 秒</a-descriptions-item><a-descriptions-item label="设备">{{ currentCase.run.device }}</a-descriptions-item><a-descriptions-item label="模型">{{ currentCase.run.model }}</a-descriptions-item></a-descriptions><a-divider /><a-space direction="vertical"><a-button type="link" :href="artifactUrl(`${currentCase.artifactRoot}/detections.json`)" target="_blank"><FileImageOutlined />检测 JSON</a-button><a-button type="link" :href="artifactUrl(`${currentCase.artifactRoot}/run_metadata.json`)" target="_blank"><InfoCircleOutlined />运行元数据</a-button></a-space></section></a-col><a-col :xs="24" :xl="16"><section class="surface-section"><div class="section-heading"><div><h2>当前影像检测明细</h2><p>边界框为 JPEG 像素坐标,不含可用地理坐标。</p></div></div><a-table :data-source="selectedImage.detections" :pagination="false" row-key="bbox_xyxy" size="small" :scroll="{ x: 680 }"><a-table-column title="类别" data-index="class_name" key="class_name" /><a-table-column title="置信度" key="confidence" align="right"><template #default="{ record }">{{ (record.confidence * 100).toFixed(1) }}%</template></a-table-column><a-table-column title="像素框 x1, y1, x2, y2" key="bbox"><template #default="{ record }">{{ record.bbox_xyxy.map((value: number) => value.toFixed(1)).join(', ') }}</template></a-table-column><template #emptyText>该影像没有达到当前阈值的候选目标。</template></a-table></section></a-col></a-row>
+ </div>
</template>
</template>
--
Gitblit v1.9.3