Parcourir la source

feat: 添加隐患上报结果页面并调整 hazard 检测流程

1.  新增 /tips-report 路由页面,用于展示隐患分析结果与历史任务
2.  移除 hazard 检测页面的本地结果展示逻辑
3.  调整 hazard 检测提交后跳转至结果页面
4.  优化 hazard 检测页面的布局样式
Stclair il y a 3 semaines
Parent
commit
7de58df05a
3 fichiers modifiés avec 533 ajouts et 69 suppressions
  1. 5 0
      src/router/index.js
  2. 8 69
      src/views/HazardDetection.vue
  3. 520 0
      src/views/tips_report.vue

+ 5 - 0
src/router/index.js

@@ -24,6 +24,11 @@ const routes = [
     path: '/knowledge',
     name: 'Knowledge',
     component: () => import('@/views/Knowledge.vue')
+  },
+  {
+    path: '/tips-report',
+    name: 'TipsReport',
+    component: () => import('@/views/tips_report.vue')
   }
 ]
 

+ 8 - 69
src/views/HazardDetection.vue

@@ -32,31 +32,16 @@
           {{ isSubmitting ? '分析中...' : '提交分析' }}
         </button>
       </div>
-      <div class="result-section" v-if="result">
-        <h3>分析结果</h3>
-        <div class="result-card">
-          <div class="risk-level" :class="result.riskLevel">
-            风险等级:{{ result.riskLevel }}
-          </div>
-          <div class="analysis-content">
-            <h4>隐患分析</h4>
-            <p>{{ result.analysis }}</p>
-          </div>
-          <div class="suggestions">
-            <h4>整改建议</h4>
-            <ul>
-              <li v-for="(s, i) in result.suggestions" :key="i">{{ s }}</li>
-            </ul>
-          </div>
-        </div>
-      </div>
     </div>
   </div>
 </template>
 
 <script setup>
 import { ref } from 'vue'
-import { getHazardTypes, getMockHazardAnalysis } from '@/mock/hazardMock'
+import { useRouter } from 'vue-router'
+import { getHazardTypes } from '@/mock/hazardMock'
+
+const router = useRouter()
 
 const formData = ref({
   type: '',
@@ -66,7 +51,6 @@ const formData = ref({
 
 const hazardTypes = ref(getHazardTypes())
 const isSubmitting = ref(false)
-const result = ref(null)
 
 const triggerUpload = () => {
   // Mock upload
@@ -79,8 +63,8 @@ const submitHazard = async () => {
   }
   isSubmitting.value = true
   setTimeout(() => {
-    result.value = getMockHazardAnalysis(formData.value)
     isSubmitting.value = false
+    router.push('/tips-report')
   }, 1500)
 }
 </script>
@@ -109,12 +93,11 @@ const submitHazard = async () => {
 }
 
 .hazard-content {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 24px;
+  max-width: 600px;
+  margin: 0 auto;
 }
 
-.form-section, .result-section {
+.form-section {
   background: #fff;
   padding: 24px;
   border-radius: 12px;
@@ -183,48 +166,4 @@ const submitHazard = async () => {
     cursor: not-allowed;
   }
 }
-
-.result-card {
-  .risk-level {
-    padding: 8px 16px;
-    border-radius: 6px;
-    font-weight: 500;
-    margin-bottom: 16px;
-
-    &.高 {
-      background: #fff1f0;
-      color: #ff4d4f;
-    }
-    &.中 {
-      background: #fffbe6;
-      color: #faad14;
-    }
-    &.低 {
-      background: #f6ffed;
-      color: #52c41a;
-    }
-  }
-
-  h4 {
-    font-size: 14px;
-    color: #333;
-    margin-bottom: 8px;
-  }
-
-  .analysis-content {
-    margin-bottom: 16px;
-    padding-bottom: 16px;
-    border-bottom: 1px solid #f0f0f0;
-  }
-
-  .suggestions ul {
-    padding-left: 20px;
-
-    li {
-      margin-bottom: 8px;
-      color: #666;
-      font-size: 14px;
-    }
-  }
-}
 </style>

+ 520 - 0
src/views/tips_report.vue

@@ -0,0 +1,520 @@
+<template>
+  <div class="tips-report-container">
+    <div class="left-sidebar">
+      <div class="sidebar-header">
+        <h3>历史记录</h3>
+        <button class="new-task-btn">+ 新建任务</button>
+      </div>
+      <div class="task-list">
+        <div
+          v-for="task in taskList"
+          :key="task.id"
+          :class="['task-item', { active: currentTaskId === task.id }]"
+          @click="selectTask(task)"
+        >
+          <div class="task-image">
+            <img :src="task.image" alt="任务图片" />
+          </div>
+          <div class="task-info">
+            <span class="task-title">{{ task.title }}</span>
+            <span class="task-time">{{ task.time }}</span>
+          </div>
+          <span :class="['task-status', task.status]">{{ task.statusText }}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="main-content">
+      <div class="content-header">
+        <h2>隐患提示</h2>
+        <div class="header-right">
+          <button class="btn-add-tag">+ 添加站</button>
+          <span class="current-time">6月12日 15:52</span>
+        </div>
+      </div>
+
+      <div class="content-body">
+        <div class="center-panel">
+          <div class="image-section">
+            <img :src="currentTask.image" alt="现场图片" class="main-image" />
+            <div class="image-nav">
+              <span class="nav-info">1 / 6</span>
+            </div>
+            <!-- 标注框 -->
+            <div
+              v-for="(tag, index) in currentTask.tags"
+              :key="index"
+              :style="{ top: tag.top, left: tag.left }"
+              class="tag-box"
+              @click="selectTag(tag)"
+            >
+              {{ tag.label }}
+            </div>
+          </div>
+
+          <div class="ai-section">
+            <div class="section-header">
+              <span class="section-title">隐患提示说明</span>
+              <!-- <span class="ai-badge">AI安全提醒</span> -->
+            </div>
+            <div class="ai-content">
+              <p>{{ currentTask.aiWarning }}</p>
+            </div>
+          </div>
+        </div>
+
+        <div class="right-panel">
+          <div class="panel-section">
+            <div class="section-header">
+              <span class="section-title">建议整改清单</span>
+            </div>
+            <div class="checklist">
+              <div
+                v-for="(item, index) in currentTask.checklist"
+                :key="index"
+                :class="['checklist-item', { checked: item.checked }]"
+              >
+                <input type="checkbox" :checked="item.checked" @change="toggleCheck(index)" />
+                <span>{{ item.text }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+
+const taskList = ref([
+  {
+    id: 1,
+    title: '加油站',
+    time: '6月12日 17:12',
+    status: 'completed',
+    statusText: '已完成',
+    image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=gas%20station%20outdoor%20view&image_size=landscape_4_3',
+    aiWarning: '综合安全风险提示:图中出现加油机、加油枪、防撞柱、加油机输油管、配电箱、灭火器等关键设备设施。经AI图像智能分析,识别出以下安全隐患风险点:加油机周边地面存在疑似油污痕迹,需及时清理以防人员滑倒;配电箱外部存在轻微破损,建议检查内部线路防护状况;消防灭火器放置位置虽符合规范,但需确认压力值是否在正常范围;防撞柱反光标识存在局部褪色,建议定期更换以保障夜间警示效果。综合评估当前场景整体安全状况为中等风险,建议立即安排专业人员对上述隐患点进行逐一排查整改,确保加油站运营安全。',
+    tags: [
+      { label: '加油机', top: '55%', left: '32%' },
+      { label: '加油机', top: '55%', left: '45%' },
+      { label: '加油机', top: '55%', left: '52%' },
+      { label: '防撞柱', top: '75%', left: '28%' },
+      { label: '防撞柱', top: '75%', left: '62%' },
+      { label: '防撞柱', top: '75%', left: '68%' },
+      { label: '配电箱', top: '45%', left: '18%' },
+    ],
+    checklist: [
+      { text: '请检查外观是否整洁,是否存在油污堆积;检查外观是否整洁,是否存在油污堆积;检查外观是否整洁,是否存在油污堆积', checked: false },
+      { text: '请确认加油机输油管及阀门连接是否牢固,定期进行紧固检查,以防泄漏或松动;检查外观是否整洁,是否存在油污堆积', checked: false },
+      { text: '请检查灭火器是否在有效期内,压力是否正常,放置位置是否便于取用;检查外观是否整洁,是否存在油污堆积', checked: false },
+      { text: '请检查配电箱及线路是否规范,是否存在私拉乱接;检查外观是否整洁,是否存在油污堆积', checked: false },
+      { text: '请检查防撞柱是否完好,高度是否符合要求,表面反光标识是否清晰;检查外观是否整洁,是否存在油污堆积', checked: false },
+    ]
+  },
+  {
+    id: 2,
+    title: '隧道工程',
+    time: '6月12日 15:53',
+    status: 'processing',
+    statusText: '处理中',
+    image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=tunnel%20construction%20site&image_size=landscape_4_3',
+    aiWarning: '隧道施工风险提示:检测到隧道内存在施工设备、临时支护结构等,建议关注支护稳定性、通风系统运行状态及照明设施完好情况。',
+    tags: [],
+    checklist: []
+  },
+  {
+    id: 3,
+    title: '加油站',
+    time: '6月12日 15:52',
+    status: 'processing',
+    statusText: '处理中',
+    image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=gas%20station%20outdoor%20view%202&image_size=landscape_4_3',
+    aiWarning: '综合安全风险提示:检测到多个加油机位、消防设施及电气设备,建议全面检查设备运行状态及安全防护措施。',
+    tags: [],
+    checklist: []
+  },
+  {
+    id: 4,
+    title: '加油站',
+    time: '6月12日 15:49',
+    status: 'pending',
+    statusText: '待处理',
+    image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=gas%20station%20equipment&image_size=landscape_4_3',
+    aiWarning: '',
+    tags: [],
+    checklist: []
+  },
+  {
+    id: 5,
+    title: '加油站',
+    time: '6月12日 15:48',
+    status: 'pending',
+    statusText: '待处理',
+    image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=gas%20station%20fuel%20dispenser&image_size=landscape_4_3',
+    aiWarning: '',
+    tags: [],
+    checklist: []
+  },
+  {
+    id: 6,
+    title: '桥梁工程',
+    time: '6月12日 15:48',
+    status: 'pending',
+    statusText: '待处理',
+    image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=bridge%20construction%20site&image_size=landscape_4_3',
+    aiWarning: '',
+    tags: [],
+    checklist: []
+  },
+])
+
+const currentTaskId = ref(1)
+
+const currentTask = computed(() => {
+  return taskList.value.find(t => t.id === currentTaskId.value) || taskList.value[0]
+})
+
+const selectTask = (task) => {
+  currentTaskId.value = task.id
+}
+
+const selectTag = (tag) => {
+  console.log('Selected tag:', tag)
+}
+
+const toggleCheck = (index) => {
+  if (currentTask.value.checklist[index]) {
+    currentTask.value.checklist[index].checked = !currentTask.value.checklist[index].checked
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.tips-report-container {
+  display: flex;
+  height: 100%;
+  background: #f5f7fa;
+}
+
+.left-sidebar {
+  width: 280px;
+  background: #fff;
+  border-right: 1px solid #e8e8e8;
+  display: flex;
+  flex-direction: column;
+}
+
+.sidebar-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #e8e8e8;
+
+  h3 {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333;
+  }
+
+  .new-task-btn {
+    padding: 6px 16px;
+    background: #1890ff;
+    color: #fff;
+    border: none;
+    border-radius: 4px;
+    font-size: 13px;
+    cursor: pointer;
+  }
+}
+
+.task-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 12px;
+}
+
+.task-item {
+  display: flex;
+  align-items: center;
+  padding: 12px;
+  margin-bottom: 8px;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid transparent;
+
+  &:hover {
+    background: #f5f7fa;
+  }
+
+  &.active {
+    background: #e6f7ff;
+    border-color: #1890ff;
+  }
+
+  .task-image {
+    width: 50px;
+    height: 50px;
+    border-radius: 6px;
+    overflow: hidden;
+    margin-right: 12px;
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+
+  .task-info {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+
+    .task-title {
+      font-size: 14px;
+      color: #333;
+      margin-bottom: 4px;
+    }
+
+    .task-time {
+      font-size: 12px;
+      color: #999;
+    }
+  }
+
+  .task-status {
+    padding: 2px 8px;
+    border-radius: 4px;
+    font-size: 12px;
+
+    &.completed {
+      background: #f6ffed;
+      color: #52c41a;
+    }
+
+    &.processing {
+      background: #fffbe6;
+      color: #faad14;
+    }
+
+    &.pending {
+      background: #f5f5f5;
+      color: #666;
+    }
+  }
+}
+
+.main-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 20px;
+  overflow: hidden;
+}
+
+.content-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  flex-shrink: 0;
+
+  h2 {
+    font-size: 20px;
+    color: #333;
+  }
+
+  .header-right {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+
+    .btn-add-tag {
+      padding: 6px 16px;
+      background: #1890ff;
+      color: #fff;
+      border: none;
+      border-radius: 4px;
+      font-size: 13px;
+      cursor: pointer;
+    }
+
+    .current-time {
+      font-size: 14px;
+      color: #999;
+    }
+  }
+}
+
+.content-body {
+  display: flex;
+  gap: 20px;
+  flex: 1;
+  overflow: hidden;
+}
+
+.center-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  overflow: hidden;
+}
+
+.image-section {
+  position: relative;
+  background: #fff;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+  height: calc(100vh * 5 / 7);
+  flex-shrink: 0;
+
+  .main-image {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+
+  .image-nav {
+    position: absolute;
+    bottom: 16px;
+    right: 16px;
+    padding: 4px 12px;
+    background: rgba(0,0,0,0.6);
+    color: #fff;
+    border-radius: 4px;
+    font-size: 12px;
+
+    .nav-info {
+      margin-right: 12px;
+    }
+  }
+
+  .tag-box {
+    position: absolute;
+    padding: 4px 10px;
+    background: #1890ff;
+    color: #fff;
+    border-radius: 4px;
+    font-size: 12px;
+    cursor: pointer;
+    transform: translate(-50%, -50%);
+
+    &:hover {
+      background: #40a9ff;
+    }
+  }
+}
+
+.ai-section {
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+  flex-shrink: 0;
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 12px;
+
+    .section-title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333;
+    }
+
+    .ai-badge {
+      padding: 2px 8px;
+      background: #f6ffed;
+      color: #52c41a;
+      border-radius: 4px;
+      font-size: 12px;
+    }
+  }
+
+  .ai-content {
+    background: #f6ffed;
+    padding: 12px 16px;
+    border-radius: 8px;
+
+    p {
+      font-size: 14px;
+      color: #666;
+      line-height: 1.6;
+      margin: 0;
+    }
+  }
+}
+
+.right-panel {
+  width: 360px;
+  flex-shrink: 0;
+}
+
+.panel-section {
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 12px;
+    flex-shrink: 0;
+
+    .section-title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333;
+    }
+  }
+
+  .checklist {
+    flex: 1;
+    overflow-y: auto;
+
+    .checklist-item {
+      display: flex;
+      align-items: flex-start;
+      padding: 10px 0;
+      border-bottom: 1px solid #f5f5f5;
+
+      &:last-child {
+        border-bottom: none;
+      }
+
+      input {
+        margin-right: 12px;
+        margin-top: 2px;
+        cursor: pointer;
+        flex-shrink: 0;
+      }
+
+      span {
+        font-size: 13px;
+        color: #666;
+        line-height: 1.5;
+      }
+
+      &.checked span {
+        text-decoration: line-through;
+        color: #999;
+      }
+    }
+  }
+}
+</style>