|
|
@@ -110,53 +110,12 @@
|
|
|
</p>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 步骤一:选择场景 -->
|
|
|
+ <!-- 步骤一:上传图片 -->
|
|
|
<div class="step-section">
|
|
|
- <h4>步骤一:选择场景</h4>
|
|
|
+ <h4>步骤一:上传需要识别的场景图片</h4>
|
|
|
<p class="step-description">
|
|
|
- 请先选择您要识别的工程场景
|
|
|
+ 系统将自动识别场景与关键要素,无需手动选择
|
|
|
</p>
|
|
|
- <div class="scenario-tags">
|
|
|
- <div
|
|
|
- v-for="(scenario, key) in scenarios"
|
|
|
- :key="key"
|
|
|
- :class="[
|
|
|
- 'scenario-tag',
|
|
|
- {
|
|
|
- active:
|
|
|
- selectedScenario === key,
|
|
|
- disabled:
|
|
|
- key !== 'gas_station' &&
|
|
|
- key !==
|
|
|
- 'simple_supported_bridge' &&
|
|
|
- key !== 'tunnel' &&
|
|
|
- key !==
|
|
|
- 'special_equipment' &&
|
|
|
- key !== 'operate_highway',
|
|
|
- 'identifying-disabled':
|
|
|
- isIdentifying,
|
|
|
- },
|
|
|
- ]"
|
|
|
- @click="
|
|
|
- !isIdentifying &&
|
|
|
- (key === 'gas_station' ||
|
|
|
- key ===
|
|
|
- 'simple_supported_bridge' ||
|
|
|
- key === 'tunnel' ||
|
|
|
- key === 'special_equipment' ||
|
|
|
- key === 'operate_highway')
|
|
|
- ? selectScenario(key)
|
|
|
- : null
|
|
|
- "
|
|
|
- >
|
|
|
- {{ scenario.name }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 步骤二:上传图片 -->
|
|
|
- <div class="step-section">
|
|
|
- <h4>步骤二:上传需要识别的场景图片</h4>
|
|
|
<div
|
|
|
class="upload-area"
|
|
|
@click="triggerFileUpload"
|
|
|
@@ -442,9 +401,10 @@
|
|
|
</span>
|
|
|
</div>
|
|
|
|
|
|
- <div class="image-container">
|
|
|
+ <div class="image-container" ref="imageContainerRef">
|
|
|
<!-- 显示图片:扫描时显示原图,扫描完成后显示标注图 -->
|
|
|
<img
|
|
|
+ ref="mainImageRef"
|
|
|
:src="
|
|
|
showScanningEffect
|
|
|
? uploadedImageUrl
|
|
|
@@ -461,9 +421,42 @@
|
|
|
cursor: pointer;
|
|
|
transform: none !important;
|
|
|
"
|
|
|
+ @load="handleMainImageLoad"
|
|
|
@error="handleMainImageError"
|
|
|
/>
|
|
|
|
|
|
+ <div
|
|
|
+ v-if="
|
|
|
+ !showScanningEffect &&
|
|
|
+ selectedKeyElement &&
|
|
|
+ elementOverlayStyle
|
|
|
+ "
|
|
|
+ ref="elementCardRef"
|
|
|
+ class="element-overlay-card"
|
|
|
+ :style="elementOverlayStyle"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <div class="element-card-title">
|
|
|
+ 当前选中:{{ selectedKeyElement }}
|
|
|
+ </div>
|
|
|
+ <ul
|
|
|
+ v-if="filteredHazards.length"
|
|
|
+ class="element-card-list"
|
|
|
+ >
|
|
|
+ <li
|
|
|
+ v-for="(
|
|
|
+ hazard, index
|
|
|
+ ) in filteredHazards"
|
|
|
+ :key="index"
|
|
|
+ >
|
|
|
+ {{ hazard }}
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ <div v-else class="element-card-empty">
|
|
|
+ 暂无对应隐患
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 扫描效果覆盖层 -->
|
|
|
<div
|
|
|
v-if="showScanningEffect"
|
|
|
@@ -534,11 +527,7 @@
|
|
|
: "未知场景"
|
|
|
}}</span
|
|
|
>场景,检测到的关键要素为<span
|
|
|
- v-for="(
|
|
|
- label, index
|
|
|
- ) in detectionResult.labels?.split(
|
|
|
- '、'
|
|
|
- ) || []"
|
|
|
+ v-for="(label, index) in displayLabels"
|
|
|
:key="index"
|
|
|
class="label-tag"
|
|
|
>{{ label }}</span
|
|
|
@@ -554,6 +543,44 @@
|
|
|
根据安全规范和施工标准,我为您梳理出以下需要重点关注的安全隐患
|
|
|
</p>
|
|
|
|
|
|
+ <div
|
|
|
+ v-if="
|
|
|
+ !isStreamingAnalysis &&
|
|
|
+ detectionResult &&
|
|
|
+ keyElements.length
|
|
|
+ "
|
|
|
+ class="key-elements-section"
|
|
|
+ >
|
|
|
+ <div class="key-elements-title">
|
|
|
+ 关键要素
|
|
|
+ </div>
|
|
|
+ <div class="key-elements-buttons">
|
|
|
+ <button
|
|
|
+ v-for="element in keyElements"
|
|
|
+ :key="element"
|
|
|
+ :class="[
|
|
|
+ 'key-element-btn',
|
|
|
+ {
|
|
|
+ active:
|
|
|
+ selectedKeyElement ===
|
|
|
+ element,
|
|
|
+ },
|
|
|
+ ]"
|
|
|
+ @click="
|
|
|
+ toggleKeyElement(element)
|
|
|
+ "
|
|
|
+ >
|
|
|
+ {{ element }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="!selectedKeyElement"
|
|
|
+ class="key-elements-hint"
|
|
|
+ >
|
|
|
+ 点击关键要素可查看对应隐患
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 场景隐患列表 -->
|
|
|
<div
|
|
|
class="hazards-section"
|
|
|
@@ -579,11 +606,16 @@
|
|
|
v-else
|
|
|
class="hazard-cards-container"
|
|
|
>
|
|
|
+ <div
|
|
|
+ v-if="!filteredHazards.length"
|
|
|
+ class="hazard-empty"
|
|
|
+ >
|
|
|
+ 暂无对应隐患
|
|
|
+ </div>
|
|
|
<div
|
|
|
v-for="(
|
|
|
hazard, index
|
|
|
- ) in detectionResult?.third_scenes ||
|
|
|
- []"
|
|
|
+ ) in filteredHazards"
|
|
|
:key="index"
|
|
|
class="hazard-card"
|
|
|
:class="{
|
|
|
@@ -1095,7 +1127,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted, computed } from "vue";
|
|
|
+import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from "vue";
|
|
|
import { ElMessage } from "element-plus";
|
|
|
import { Upload, View, DataAnalysis, Bell } from "@element-plus/icons-vue";
|
|
|
import Sidebar from "@/components/Sidebar.vue";
|
|
|
@@ -1106,7 +1138,7 @@ import startIdentifyActiveImg from "@/assets/Hazard/3.png";
|
|
|
|
|
|
// 响应式数据
|
|
|
const messageText = ref("");
|
|
|
-const selectedScenario = ref("tunnel"); // 默认选择隧道工程
|
|
|
+const selectedScenario = ref(""); // 自动识别出的场景
|
|
|
const uploadedImage = ref(null);
|
|
|
const uploadedImageUrl = ref(""); // 新增:存储上传后的图片URL
|
|
|
const fileInput = ref(null);
|
|
|
@@ -1145,6 +1177,11 @@ const isStreamingAnalysis = ref(false); // 控制整个分析文本流式输出
|
|
|
const streamingAnalysis = ref(""); // 流式输出的完整分析文本
|
|
|
const showAnalysisPrompt = ref(false); // 控制分析提示显示
|
|
|
const visibleHazardCards = ref({}); // 控制每个隐患卡片的显示状态
|
|
|
+const selectedKeyElement = ref(null); // 当前选中的关键要素
|
|
|
+const imageContainerRef = ref(null); // 图片容器引用
|
|
|
+const mainImageRef = ref(null); // 主图引用
|
|
|
+const elementCardRef = ref(null); // 关键要素卡片引用
|
|
|
+const elementOverlayStyle = ref(null); // 关键要素卡片定位样式
|
|
|
|
|
|
// 历史记录相关数据
|
|
|
const historyData = ref([]); // 存储历史记录数据
|
|
|
@@ -1192,6 +1229,14 @@ const scenarios = {
|
|
|
operate_highway: { name: "运营高速公路", color: "#722ED1" },
|
|
|
};
|
|
|
|
|
|
+const autoSceneOrder = [
|
|
|
+ "tunnel",
|
|
|
+ "simple_supported_bridge",
|
|
|
+ "gas_station",
|
|
|
+ "special_equipment",
|
|
|
+ "operate_highway",
|
|
|
+];
|
|
|
+
|
|
|
// 历史记录数据结构 - 初始为空数组,等待后端数据
|
|
|
// const historyData = ref([]);
|
|
|
|
|
|
@@ -1239,6 +1284,243 @@ const getTagText = (tagType) => {
|
|
|
return tagTypeConfig[tagType]?.text || "隧道";
|
|
|
};
|
|
|
|
|
|
+const getUniqueLabels = (labels) => {
|
|
|
+ const seen = new Set();
|
|
|
+ return labels.filter((label) => {
|
|
|
+ if (!label || seen.has(label)) return false;
|
|
|
+ seen.add(label);
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const displayLabels = computed(() => {
|
|
|
+ const labels =
|
|
|
+ detectionResult.value?.display_labels || detectionResult.value?.labels;
|
|
|
+ if (!labels) return [];
|
|
|
+
|
|
|
+ const normalizedLabels = (Array.isArray(labels) ? labels : String(labels).split(/[,,、]/))
|
|
|
+ .map((label) => normalizeLabel(String(label).trim()))
|
|
|
+ .filter((label) => label);
|
|
|
+
|
|
|
+ return getUniqueLabels(normalizedLabels);
|
|
|
+});
|
|
|
+
|
|
|
+const keyElements = computed(() => displayLabels.value);
|
|
|
+
|
|
|
+const normalizeLabel = (label) => {
|
|
|
+ if (!label) return "";
|
|
|
+ const parts = String(label).split("_").filter((part) => part);
|
|
|
+ if (parts.length <= 1) return String(label);
|
|
|
+ return parts.slice(1).join("_");
|
|
|
+};
|
|
|
+
|
|
|
+const hazardMatchesElement = (hazard, element) => {
|
|
|
+ if (!hazard || !element) return false;
|
|
|
+ return hazard.includes(element) || element.includes(hazard);
|
|
|
+};
|
|
|
+
|
|
|
+const hazardsMap = computed(() => {
|
|
|
+ const map = {};
|
|
|
+ keyElements.value.forEach((element) => {
|
|
|
+ map[element] = [];
|
|
|
+ });
|
|
|
+ const hazards = detectionResult.value?.third_scenes || [];
|
|
|
+ hazards.forEach((hazard) => {
|
|
|
+ let matched = false;
|
|
|
+ for (const element of keyElements.value) {
|
|
|
+ if (hazardMatchesElement(hazard, element)) {
|
|
|
+ map[element].push(hazard);
|
|
|
+ matched = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!matched) {
|
|
|
+ if (!map.__unmatched) map.__unmatched = [];
|
|
|
+ map.__unmatched.push(hazard);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+});
|
|
|
+
|
|
|
+const filteredHazards = computed(() => {
|
|
|
+ if (!detectionResult.value) return [];
|
|
|
+ if (!selectedKeyElement.value) {
|
|
|
+ return detectionResult.value?.third_scenes || [];
|
|
|
+ }
|
|
|
+ const backendHazards =
|
|
|
+ detectionResult.value?.element_hazards?.[selectedKeyElement.value];
|
|
|
+ if (Array.isArray(backendHazards)) {
|
|
|
+ return backendHazards;
|
|
|
+ }
|
|
|
+ return hazardsMap.value[selectedKeyElement.value] || [];
|
|
|
+});
|
|
|
+
|
|
|
+const selectedDetection = computed(() => {
|
|
|
+ if (!selectedKeyElement.value) return null;
|
|
|
+ const detections = detectionResult.value?.detections || [];
|
|
|
+ return (
|
|
|
+ detections.find((detection) => {
|
|
|
+ const label = normalizeLabel(detection?.label || "");
|
|
|
+ return (
|
|
|
+ label === selectedKeyElement.value ||
|
|
|
+ label.includes(selectedKeyElement.value) ||
|
|
|
+ selectedKeyElement.value.includes(label)
|
|
|
+ );
|
|
|
+ }) || null
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+const resetKeyElementState = () => {
|
|
|
+ selectedKeyElement.value = null;
|
|
|
+ elementOverlayStyle.value = null;
|
|
|
+};
|
|
|
+
|
|
|
+const toggleKeyElement = async (element) => {
|
|
|
+ if (selectedKeyElement.value === element) {
|
|
|
+ resetKeyElementState();
|
|
|
+ } else {
|
|
|
+ selectedKeyElement.value = element;
|
|
|
+ }
|
|
|
+ await nextTick();
|
|
|
+ updateElementOverlayPosition();
|
|
|
+};
|
|
|
+
|
|
|
+const setVisibleHazardCards = (hazards = filteredHazards.value) => {
|
|
|
+ const nextVisibleHazards = {};
|
|
|
+ hazards.forEach((_, index) => {
|
|
|
+ nextVisibleHazards[index] = true;
|
|
|
+ });
|
|
|
+ visibleHazardCards.value = nextVisibleHazards;
|
|
|
+};
|
|
|
+
|
|
|
+const getAutoSceneCandidates = () => {
|
|
|
+ if (
|
|
|
+ selectedScenario.value &&
|
|
|
+ autoSceneOrder.includes(selectedScenario.value)
|
|
|
+ ) {
|
|
|
+ return [
|
|
|
+ selectedScenario.value,
|
|
|
+ ...autoSceneOrder.filter(
|
|
|
+ (sceneKey) => sceneKey !== selectedScenario.value
|
|
|
+ ),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return autoSceneOrder;
|
|
|
+};
|
|
|
+
|
|
|
+const detectSceneAutomatically = async (baseRequestData) => {
|
|
|
+ let lastErrorMessage = "";
|
|
|
+
|
|
|
+ for (const sceneKey of getAutoSceneCandidates()) {
|
|
|
+ try {
|
|
|
+ const response = await apis.hazardDetection({
|
|
|
+ ...baseRequestData,
|
|
|
+ scene_name: sceneKey,
|
|
|
+ });
|
|
|
+ const isSuccess =
|
|
|
+ response.code === 200 || response.statusCode === 200;
|
|
|
+
|
|
|
+ if (isSuccess) {
|
|
|
+ return {
|
|
|
+ response,
|
|
|
+ sceneKey,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ lastErrorMessage =
|
|
|
+ response.msg || response.message || lastErrorMessage;
|
|
|
+ } catch (error) {
|
|
|
+ lastErrorMessage =
|
|
|
+ error?.msg || error?.message || lastErrorMessage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error(
|
|
|
+ lastErrorMessage || "暂未识别到支持的场景,请尝试更换更清晰的图片"
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const updateElementOverlayPosition = async () => {
|
|
|
+ if (
|
|
|
+ !selectedKeyElement.value ||
|
|
|
+ showScanningEffect.value ||
|
|
|
+ currentView.value !== "detail"
|
|
|
+ ) {
|
|
|
+ elementOverlayStyle.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const container = imageContainerRef.value;
|
|
|
+ const imageElement = mainImageRef.value;
|
|
|
+ const detection = selectedDetection.value;
|
|
|
+
|
|
|
+ if (
|
|
|
+ !container ||
|
|
|
+ !imageElement ||
|
|
|
+ !detection ||
|
|
|
+ !Array.isArray(detection.box) ||
|
|
|
+ detection.box.length < 4 ||
|
|
|
+ !imageElement.naturalWidth ||
|
|
|
+ !imageElement.naturalHeight
|
|
|
+ ) {
|
|
|
+ elementOverlayStyle.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const containerWidth = container.clientWidth;
|
|
|
+ const containerHeight = container.clientHeight;
|
|
|
+ const naturalWidth = imageElement.naturalWidth;
|
|
|
+ const naturalHeight = imageElement.naturalHeight;
|
|
|
+
|
|
|
+ if (!containerWidth || !containerHeight) {
|
|
|
+ elementOverlayStyle.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const scale = Math.min(
|
|
|
+ containerWidth / naturalWidth,
|
|
|
+ containerHeight / naturalHeight
|
|
|
+ );
|
|
|
+ const renderedWidth = naturalWidth * scale;
|
|
|
+ const renderedHeight = naturalHeight * scale;
|
|
|
+ const offsetX = (containerWidth - renderedWidth) / 2;
|
|
|
+ const offsetY = (containerHeight - renderedHeight) / 2;
|
|
|
+
|
|
|
+ const [x1, y1, x2, y2] = detection.box.map((value) => Number(value) || 0);
|
|
|
+
|
|
|
+ const boxLeft = offsetX + x1 * scale;
|
|
|
+ const boxTop = offsetY + y1 * scale;
|
|
|
+ const boxRight = offsetX + x2 * scale;
|
|
|
+
|
|
|
+ let cardLeft = boxRight + 12;
|
|
|
+ let cardTop = boxTop;
|
|
|
+ const cardWidth = elementCardRef.value?.offsetWidth || 260;
|
|
|
+ const cardHeight = elementCardRef.value?.offsetHeight || 148;
|
|
|
+ const safePadding = 12;
|
|
|
+
|
|
|
+ if (cardLeft + cardWidth > containerWidth - safePadding) {
|
|
|
+ cardLeft = Math.max(safePadding, boxLeft - cardWidth - 12);
|
|
|
+ }
|
|
|
+ if (cardTop + cardHeight > containerHeight - safePadding) {
|
|
|
+ cardTop = Math.max(
|
|
|
+ safePadding,
|
|
|
+ containerHeight - cardHeight - safePadding
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ elementOverlayStyle.value = {
|
|
|
+ left: `${cardLeft}px`,
|
|
|
+ top: `${Math.max(safePadding, cardTop)}px`,
|
|
|
+ };
|
|
|
+
|
|
|
+ await nextTick();
|
|
|
+};
|
|
|
+
|
|
|
+const handleMainImageLoad = async () => {
|
|
|
+ await nextTick();
|
|
|
+ updateElementOverlayPosition();
|
|
|
+};
|
|
|
+
|
|
|
// 删除历史记录
|
|
|
const deleteHistoryItem = (historyItem, index) => {
|
|
|
console.log("准备删除隐患提示历史记录:", historyItem);
|
|
|
@@ -1306,7 +1588,7 @@ const createNewChat = () => {
|
|
|
|
|
|
// 重置所有状态
|
|
|
currentView.value = "main";
|
|
|
- selectedScenario.value = "tunnel"; // 默认选择隧道工程
|
|
|
+ selectedScenario.value = "";
|
|
|
uploadedImage.value = null;
|
|
|
uploadedImageUrl.value = "";
|
|
|
selectedHistoryItem.value = null;
|
|
|
@@ -1328,6 +1610,8 @@ const createNewChat = () => {
|
|
|
isStreamingAnalysis.value = false; // 重置分析文本流式输出状态
|
|
|
streamingAnalysis.value = ""; // 清空分析文本流式输出内容
|
|
|
showAnalysisPrompt.value = false; // 重置分析提示状态
|
|
|
+ detectionResult.value = null;
|
|
|
+ resetKeyElementState();
|
|
|
// 清空文件输入
|
|
|
if (fileInput.value) {
|
|
|
fileInput.value.value = "";
|
|
|
@@ -1380,13 +1664,18 @@ const handleHistoryItem = async (historyItem, event) => {
|
|
|
detailData.tag_type ||
|
|
|
getTagTypeFromLabels(detailData.labels),
|
|
|
labels: detailData.labels,
|
|
|
+ display_labels: detailData.display_labels || [],
|
|
|
total_detections: detailData.labels
|
|
|
? Array.isArray(detailData.labels)
|
|
|
? detailData.labels.length
|
|
|
: 0
|
|
|
: 0,
|
|
|
third_scenes: detailData.third_scenes || [],
|
|
|
+ element_hazards: detailData.element_hazards || {},
|
|
|
+ detections: detailData.detections || [],
|
|
|
};
|
|
|
+ selectedScenario.value = detectionResult.value.scene_name || "";
|
|
|
+ resetKeyElementState();
|
|
|
|
|
|
// 设置图片URL并检测加载完成
|
|
|
const newImageUrl =
|
|
|
@@ -1405,11 +1694,9 @@ const handleHistoryItem = async (historyItem, event) => {
|
|
|
historyItem.tagType = tagType;
|
|
|
|
|
|
// 显示所有隐患卡片(历史记录不需要动画效果)
|
|
|
- const hazards = detectionResult.value?.third_scenes || [];
|
|
|
- visibleHazardCards.value = {};
|
|
|
- hazards.forEach((hazard, index) => {
|
|
|
- visibleHazardCards.value[index] = true;
|
|
|
- });
|
|
|
+ setVisibleHazardCards(
|
|
|
+ detectionResult.value?.third_scenes || []
|
|
|
+ );
|
|
|
} else {
|
|
|
console.error("获取详情失败:", detailResponse.message);
|
|
|
ElMessage.error("获取记录详情失败");
|
|
|
@@ -1418,8 +1705,11 @@ const handleHistoryItem = async (historyItem, event) => {
|
|
|
detectionResult.value = {
|
|
|
scene_name: historyItem.tagType || "simple_supported_bridge",
|
|
|
labels: historyItem.labels,
|
|
|
+ display_labels: [],
|
|
|
total_detections: 0,
|
|
|
third_scenes: [],
|
|
|
+ element_hazards: {},
|
|
|
+ detections: [],
|
|
|
};
|
|
|
const fallbackImageUrl =
|
|
|
historyItem.recognitionImageUrl || historyItem.originalImageUrl;
|
|
|
@@ -1431,7 +1721,7 @@ const handleHistoryItem = async (historyItem, event) => {
|
|
|
}
|
|
|
|
|
|
// 显示所有隐患卡片
|
|
|
- visibleHazardCards.value = {};
|
|
|
+ setVisibleHazardCards([]);
|
|
|
}
|
|
|
|
|
|
// 更新数据层的active状态
|
|
|
@@ -1449,19 +1739,6 @@ const handleHistoryItem = async (historyItem, event) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 选择场景
|
|
|
-const selectScenario = (scenarioKey) => {
|
|
|
- try {
|
|
|
- console.log("selectScenario 被调用,场景:", scenarioKey);
|
|
|
- selectedScenario.value = scenarioKey;
|
|
|
- isDragOver.value = false; // 重置拖拽状态
|
|
|
- console.log("选择场景:", scenarios[scenarioKey].name);
|
|
|
- } catch (error) {
|
|
|
- console.error("选择场景失败:", error);
|
|
|
- isDragOver.value = false; // 重置拖拽状态
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
// 触发文件上传
|
|
|
const triggerFileUpload = () => {
|
|
|
try {
|
|
|
@@ -1527,13 +1804,6 @@ const startIdentification = async () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if (!selectedScenario.value) {
|
|
|
- console.log("未选择场景");
|
|
|
- ElMessage.warning("请先选择场景");
|
|
|
- isDragOver.value = false; // 重置拖拽状态
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
if (!uploadedImageUrl.value) {
|
|
|
// 使用 uploadedImageUrl.value 判断
|
|
|
console.log("未上传图片");
|
|
|
@@ -1573,13 +1843,11 @@ const startIdentification = async () => {
|
|
|
// 如果检查失败,继续执行识别流程
|
|
|
}
|
|
|
|
|
|
- console.log("开始识别:", {
|
|
|
- scenario: scenarios[selectedScenario.value].name,
|
|
|
- image: uploadedImageUrl.value, // 使用 uploadedImageUrl.value
|
|
|
- });
|
|
|
+ console.log("开始自动识别图片场景:", uploadedImageUrl.value);
|
|
|
|
|
|
// 开始识别状态
|
|
|
isIdentifying.value = true;
|
|
|
+ resetKeyElementState();
|
|
|
|
|
|
// 调用后端API进行隐患提示
|
|
|
// ElMessage.success("开始进行隐患提示,请稍候...");
|
|
|
@@ -1601,15 +1869,16 @@ const startIdentification = async () => {
|
|
|
|
|
|
const requestData = {
|
|
|
// ===== 已删除:user_id - 后端从token解析 =====
|
|
|
- scene_name: selectedScenario.value,
|
|
|
image: uploadedImageUrl.value,
|
|
|
account: accountLastFour,
|
|
|
username: username,
|
|
|
date: currentDate,
|
|
|
};
|
|
|
|
|
|
- console.log("发送隐患提示请求:", requestData);
|
|
|
- const response = await apis.hazardDetection(requestData);
|
|
|
+ console.log("发送自动场景识别请求:", requestData);
|
|
|
+ const { response, sceneKey } = await detectSceneAutomatically(
|
|
|
+ requestData
|
|
|
+ );
|
|
|
console.log("隐患提示响应:", response);
|
|
|
|
|
|
// 检查响应结构,兼容不同的字段名
|
|
|
@@ -1619,7 +1888,13 @@ const startIdentification = async () => {
|
|
|
ElMessage.success("隐患提示完成!");
|
|
|
|
|
|
// 保存识别结果
|
|
|
- detectionResult.value = response.data;
|
|
|
+ detectionResult.value = {
|
|
|
+ ...response.data,
|
|
|
+ scene_name: response.data.scene_name || sceneKey,
|
|
|
+ display_labels: response.data.display_labels || [],
|
|
|
+ element_hazards: response.data.element_hazards || {},
|
|
|
+ };
|
|
|
+ selectedScenario.value = detectionResult.value.scene_name || sceneKey;
|
|
|
|
|
|
// 处理标注后的图片
|
|
|
if (response.data.annotated_image) {
|
|
|
@@ -1654,6 +1929,10 @@ const startIdentification = async () => {
|
|
|
// 自动选中最新创建的历史记录
|
|
|
if (historyData.value.length > 0) {
|
|
|
const latestRecord = historyData.value[0]; // 假设最新的记录在数组第一位
|
|
|
+ latestRecord.tagType =
|
|
|
+ detectionResult.value.scene_name || latestRecord.tagType;
|
|
|
+ latestRecord.detections =
|
|
|
+ detectionResult.value.detections || [];
|
|
|
selectedHistoryItem.value = latestRecord;
|
|
|
|
|
|
// 更新所有记录的active状态
|
|
|
@@ -1856,14 +2135,14 @@ const startAnalysisStreaming = () => {
|
|
|
// 重置状态
|
|
|
isStreamingAnalysis.value = false;
|
|
|
streamingAnalysis.value = "";
|
|
|
+ resetKeyElementState();
|
|
|
visibleHazardCards.value = {}; // 重置隐患卡片显示状态
|
|
|
|
|
|
// 立即开始流式输出,不需要延迟
|
|
|
// 构建完整的分析文本(带HTML标签)
|
|
|
const sceneName = detectionResult.value?.scene_name;
|
|
|
const sceneText = sceneName ? scenarios[sceneName]?.name : "未知场景";
|
|
|
- const labels = detectionResult.value?.labels || "";
|
|
|
- const labelsArray = labels.split("、");
|
|
|
+ const labelsArray = displayLabels.value;
|
|
|
|
|
|
// 构建带HTML的完整文本
|
|
|
const labelsTags = labelsArray
|
|
|
@@ -1937,7 +2216,8 @@ const startAnalysisStreaming = () => {
|
|
|
const showHazardCardsSequentially = () => {
|
|
|
try {
|
|
|
console.log("开始逐个显示隐患卡片");
|
|
|
- const hazards = detectionResult.value?.third_scenes || [];
|
|
|
+ const hazards = filteredHazards.value || [];
|
|
|
+ visibleHazardCards.value = {};
|
|
|
|
|
|
// 在第一个卡片显示之前,先滚动到隐患section
|
|
|
setTimeout(() => {
|
|
|
@@ -1979,6 +2259,9 @@ const clearUploadedImage = () => {
|
|
|
console.log("clearUploadedImage 被调用");
|
|
|
uploadedImage.value = null;
|
|
|
uploadedImageUrl.value = "";
|
|
|
+ annotatedImageUrl.value = "";
|
|
|
+ detectionResult.value = null;
|
|
|
+ resetKeyElementState();
|
|
|
isDragOver.value = false; // 重置拖拽状态
|
|
|
if (fileInput.value) {
|
|
|
fileInput.value.value = ""; // 清空input的value
|
|
|
@@ -2085,7 +2368,7 @@ const processImageOrientation = (file) => {
|
|
|
const processFile = async (file) => {
|
|
|
try {
|
|
|
console.log("processFile 被调用,文件:", file);
|
|
|
- // 检查文件大小(10MB限制)
|
|
|
+ // 检查文件大小(5MB限制)
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
|
console.log("文件大小超过限制:", file.size);
|
|
|
ElMessage.error("文件大小不能超过5MB");
|
|
|
@@ -2098,7 +2381,7 @@ const processFile = async (file) => {
|
|
|
console.log("文件类型:", file.type);
|
|
|
if (!allowedTypes.includes(file.type)) {
|
|
|
console.log("不支持的文件类型:", file.type);
|
|
|
- ElMessage.error("只支持JPG、PNG、GIF、BMP、WEBP格式的图片");
|
|
|
+ ElMessage.error("只支持JPG、PNG格式的图片");
|
|
|
isDragOver.value = false; // 重置拖拽状态
|
|
|
return;
|
|
|
}
|
|
|
@@ -2469,9 +2752,53 @@ const submitEvaluation = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 页面初始化时获取历史记录
|
|
|
+watch(
|
|
|
+ filteredHazards,
|
|
|
+ (hazards) => {
|
|
|
+ if (
|
|
|
+ showScanningEffect.value ||
|
|
|
+ showAnalysisPrompt.value ||
|
|
|
+ isStreamingAnalysis.value
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setVisibleHazardCards(hazards);
|
|
|
+ },
|
|
|
+ { deep: false }
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ [
|
|
|
+ selectedKeyElement,
|
|
|
+ currentView,
|
|
|
+ showScanningEffect,
|
|
|
+ () => detectionResult.value?.detections,
|
|
|
+ ],
|
|
|
+ async () => {
|
|
|
+ if (
|
|
|
+ !selectedKeyElement.value ||
|
|
|
+ currentView.value !== "detail" ||
|
|
|
+ showScanningEffect.value
|
|
|
+ ) {
|
|
|
+ elementOverlayStyle.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await nextTick();
|
|
|
+ updateElementOverlayPosition();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+const handleWindowResize = () => {
|
|
|
+ updateElementOverlayPosition();
|
|
|
+};
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
getHistoryRecords();
|
|
|
+ window.addEventListener("resize", handleWindowResize);
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener("resize", handleWindowResize);
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -3438,6 +3765,41 @@ onMounted(() => {
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
+ .element-overlay-card {
|
|
|
+ position: absolute;
|
|
|
+ width: 260px;
|
|
|
+ max-height: 180px;
|
|
|
+ overflow-y: auto;
|
|
|
+ text-align: left;
|
|
|
+ padding: 14px 16px;
|
|
|
+ background: rgba(255, 255, 255, 0.96);
|
|
|
+ border: 1px solid rgba(239, 68, 68, 0.25);
|
|
|
+ border-radius: 14px;
|
|
|
+ box-shadow: 0 16px 32px rgba(15, 23, 42, 0.18);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ z-index: 6;
|
|
|
+
|
|
|
+ .element-card-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #b91c1c;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .element-card-list {
|
|
|
+ margin: 0;
|
|
|
+ padding-left: 18px;
|
|
|
+ color: #374151;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .element-card-empty {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.scanning-overlay {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
@@ -3626,6 +3988,57 @@ onMounted(() => {
|
|
|
margin-bottom: 0.75rem;
|
|
|
}
|
|
|
|
|
|
+ .key-elements-section {
|
|
|
+ margin-bottom: 1rem;
|
|
|
+ padding: 0.875rem 1rem;
|
|
|
+ border-radius: 16px;
|
|
|
+ background: linear-gradient(135deg, #eef4ff 0%, #f8fbff 100%);
|
|
|
+ border: 1px solid #dbeafe;
|
|
|
+
|
|
|
+ .key-elements-title {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e3a8a;
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .key-elements-buttons {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 0.625rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .key-element-btn {
|
|
|
+ border: 1px solid #bfdbfe;
|
|
|
+ background: #ffffff;
|
|
|
+ color: #1d4ed8;
|
|
|
+ border-radius: 9999px;
|
|
|
+ padding: 0.45rem 0.9rem;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ line-height: 1;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #60a5fa;
|
|
|
+ background: #eff6ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ background: #2563eb;
|
|
|
+ border-color: #2563eb;
|
|
|
+ color: #ffffff;
|
|
|
+ box-shadow: 0 10px 24px rgba(37, 99, 235, 0.22);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .key-elements-hint {
|
|
|
+ margin-top: 0.75rem;
|
|
|
+ font-size: 0.8125rem;
|
|
|
+ color: #64748b;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/* 场景隐患列表样式 - 移到analysis-section内部 */
|
|
|
.hazards-section {
|
|
|
margin-top: 0;
|
|
|
@@ -3644,6 +4057,17 @@ onMounted(() => {
|
|
|
margin-top: 0.75rem;
|
|
|
}
|
|
|
|
|
|
+ .hazard-empty {
|
|
|
+ padding: 1rem 1.25rem;
|
|
|
+ margin-top: 0.75rem;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px dashed #cbd5e1;
|
|
|
+ border-radius: 16px;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #64748b;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
.hazard-card {
|
|
|
background-color: #ffffff;
|
|
|
border-radius: 9999px;
|