3 Commits b9bbecdc3d ... 1289f6b98c

Autor SHA1 Mensagem Data
  asuka-4908 1289f6b98c 增加隐患图片收起功能 3 semanas atrás
  asuka-4908 e26c3e834b 增加查看大图功能 3 semanas atrás
  asuka-4908 d48dc6a75a 隐患提示界面重构 3 semanas atrás
2 arquivos alterados com 615 adições e 45 exclusões
  1. 0 3
      package-lock.json
  2. 615 42
      src/views/tips_report.vue

+ 0 - 3
package-lock.json

@@ -1568,7 +1568,6 @@
       "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "chokidar": "^5.0.0",
         "immutable": "^5.1.5",
@@ -1599,7 +1598,6 @@
       "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.21.3",
         "postcss": "^8.4.43",
@@ -1659,7 +1657,6 @@
       "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.35.tgz",
       "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vue/compiler-dom": "3.5.35",
         "@vue/compiler-sfc": "3.5.35",

+ 615 - 42
src/views/tips_report.vue

@@ -35,8 +35,18 @@
 
       <div class="content-body">
         <div class="center-panel">
-          <div class="image-section">
-            <img :src="currentTask.image" alt="现场图片" class="main-image" />
+          <div :class="['image-section', { collapsed: isImageCollapsed }]">
+            <div class="image-toolbar">
+              <span>图片</span>
+              <button type="button" @click="isImageCollapsed = !isImageCollapsed">
+                {{ isImageCollapsed ? '展开' : '收起' }}
+              </button>
+            </div>
+            <img v-show="!isImageCollapsed" :src="currentTask.image" alt="现场图片" class="main-image" />
+            <div v-show="isImageCollapsed" class="image-collapsed-summary">
+              <img :src="currentTask.image" alt="现场缩略图" />
+              <span>图片已收起</span>
+            </div>
           </div>
 
           <div class="ai-section">
@@ -137,22 +147,80 @@
         <div class="right-panel">
           <div class="panel-section">
             <div class="section-header">
-              <span class="section-title">建议整改清单</span>
+              <span class="section-title">检测要素</span>
+              <span class="section-count">5</span>
             </div>
             <div class="checklist">
               <div
-                v-for="(item, index) in currentTask.checklist"
-                :key="index"
-                :class="['checklist-item', { checked: item.checked }]"
+                v-for="group in detectionGroups"
+                :key="group.id"
+                :class="['check-group', { expanded: expandedGroupId === group.id }]"
               >
-                <input type="checkbox" :checked="item.checked" @change="toggleCheck(index)" />
-                <span>{{ item.text }}</span>
+                <button class="check-group-header" type="button" @click="toggleGroup(group.id)">
+                  <div class="check-group-title">
+                    <span class="group-index">{{ group.id }}</span>
+                    <span>{{ group.title }}</span>
+                  </div>
+                  <div class="group-meta">
+                    <span>{{ group.items.length }} 项</span>
+                    <span class="group-caret">▶</span>
+                  </div>
+                </button>
+                <div
+                  v-for="(item, index) in group.items"
+                  v-show="expandedGroupId === group.id"
+                  :key="`${group.id}-${index}`"
+                  class="checklist-row"
+                >
+                  <button
+                    type="button"
+                    :class="['checklist-item', { collapsed: isCaseCollapsed(group.id, index) }]"
+                    @click="toggleCase(group.id, index)"
+                  >
+                    <span class="item-index">{{ index + 1 }}</span>
+                    <span class="item-text">{{ item.text }}</span>
+                    <span class="item-caret">▼</span>
+                  </button>
+                  <div v-show="!isCaseCollapsed(group.id, index)" class="case-examples">
+                    <button class="case-view-btn" type="button" @click="openCaseViewer">
+                      查看大图
+                    </button>
+                    <div class="case-card positive">
+                      <div class="case-title">正确示例</div>
+                      <img src="/case-positive.png" alt="正确示例" />
+                    </div>
+                    <div class="case-card negative">
+                      <div class="case-title">错误示例</div>
+                      <img src="/case-negative.png" alt="错误示例" />
+                    </div>
+                  </div>
+                </div>
               </div>
             </div>
           </div>
         </div>
       </div>
     </div>
+
+    <div v-if="showCaseViewer" class="case-viewer" @click.self="closeCaseViewer">
+      <div class="case-viewer-panel">
+        <div class="case-viewer-header">
+          <span>正反案例对比</span>
+          <button type="button" class="case-viewer-close" @click="closeCaseViewer">×</button>
+        </div>
+        <div class="case-viewer-body">
+          <div class="case-viewer-card positive">
+            <div class="case-viewer-title">正确示例</div>
+            <img src="/case-positive.png" alt="正确示例" />
+          </div>
+          <div class="case-viewer-card negative">
+            <div class="case-viewer-title">错误示例</div>
+            <img src="/case-negative.png" alt="错误示例" />
+          </div>
+        </div>
+      </div>
+    </div>
+
   </div>
 </template>
 
@@ -178,11 +246,26 @@ const taskList = ref([
       { label: '配电箱', top: '45%', left: '18%' },
     ],
     checklist: [
-      { text: '请检查外观是否整洁,是否存在油污堆积;检查外观是否整洁,是否存在油污堆积;检查外观是否整洁,是否存在油污堆积', checked: false },
-      { text: '请确认加油机输油管及阀门连接是否牢固,定期进行紧固检查,以防泄漏或松动;检查外观是否整洁,是否存在油污堆积', checked: false },
-      { text: '请检查灭火器是否在有效期内,压力是否正常,放置位置是否便于取用;检查外观是否整洁,是否存在油污堆积', checked: false },
-      { text: '请检查配电箱及线路是否规范,是否存在私拉乱接;检查外观是否整洁,是否存在油污堆积', checked: false },
-      { text: '请检查防撞柱是否完好,高度是否符合要求,表面反光标识是否清晰;检查外观是否整洁,是否存在油污堆积', checked: false },
+      { text: '加油机外观不整洁,有明显油污' },
+      { text: '机座地脚螺栓明显松动' },
+      { text: '加油机紧固件连接明显松动' },
+      { text: '加油机底部填沙潮湿' },
+      { text: '加油机底部填沙有明显油迹' },
+      { text: '进油管与紧急截断阀连接处未露出沙外' },
+      { text: '油品管线连接处有明显渗漏' },
+      { text: '过滤器有明显油污和渗漏' },
+      { text: '加油机底部未设置截断阀' },
+      { text: '加油软管上未设置安全拉断阀' },
+      { text: '加油枪密封不良,工作时出现明显渗漏' },
+      { text: '汽油加油枪无集气罩' },
+      { text: '加油枪集气罩破损' },
+      { text: '油气回收检测口的丝堵松动' },
+      { text: '油气回收管路渗油' },
+      { text: '加油机静电接地失效' },
+      { text: '加油机铭牌不清晰' },
+      { text: '加油机工艺仓与电气仓之间隔板上的孔洞未密封' },
+      { text: '加油机内防爆挠性管连接明显松动' },
+      { text: '防爆接线盒未在专用接地端子处进行静电接地连接' },
     ]
   },
   {
@@ -248,8 +331,111 @@ const currentTask = computed(() => {
   return taskList.value.find(t => t.id === currentTaskId.value) || taskList.value[0]
 })
 
+const detectionGroups = computed(() => [
+  {
+    id: 1,
+    title: '加油机',
+    items: currentTask.value.checklist
+  },
+  {
+    id: 2,
+    title: '加油机输油软管',
+    items: [
+      { text: '输油软管外观存在老化、开裂或磨损' },
+      { text: '软管连接接头有明显松动' },
+      { text: '软管与加油枪连接处存在渗漏' },
+      { text: '软管与加油机连接处存在渗漏' },
+      { text: '软管未设置安全拉断阀' },
+      { text: '安全拉断阀安装方向不符合要求' },
+      { text: '安全拉断阀外观破损或锈蚀' },
+      { text: '软管拖地或受车辆碾压风险明显' },
+      { text: '软管长度不符合现场使用要求' },
+      { text: '软管表面有明显油污' },
+      { text: '软管弯折半径过小' },
+      { text: '软管固定支架缺失或损坏' },
+      { text: '软管回位装置失效' },
+      { text: '软管标识不清晰' },
+      { text: '软管未按要求定期检查' },
+      { text: '软管耐压等级标识缺失' },
+      { text: '软管附近存在明火或高温风险' },
+      { text: '软管连接密封件老化' },
+      { text: '软管防静电连接失效' },
+      { text: '软管收纳区域通行受阻' },
+    ]
+  },
+  {
+    id: 3,
+    title: '防撞栏',
+    items: [
+      { text: '防撞栏缺失或设置位置不合理' },
+      { text: '防撞栏固定螺栓松动' },
+      { text: '防撞栏立柱变形或倾斜' },
+      { text: '防撞栏表面反光标识不清晰' },
+      { text: '防撞栏警示颜色脱落' },
+      { text: '防撞栏高度不符合要求' },
+      { text: '防撞栏基础破损' },
+      { text: '防撞栏与设备间距不足' },
+      { text: '车辆通行区域防护不足' },
+      { text: '防撞设施周边存在杂物堆放' },
+      { text: '防撞栏夜间可视性不足' },
+      { text: '防撞栏维修记录缺失' },
+      { text: '防撞栏端部未做安全处理' },
+    ]
+  },
+  {
+    id: 4,
+    title: '支撑柱/立柱',
+    items: [
+      { text: '支撑柱外观存在明显裂缝' },
+      { text: '支撑柱表面防腐层脱落' },
+      { text: '立柱底座固定件松动' },
+      { text: '立柱根部存在积水或锈蚀' },
+      { text: '立柱附近防撞措施不足' },
+      { text: '立柱警示标识不清晰' },
+      { text: '立柱垂直度异常' },
+      { text: '立柱与棚体连接处异常' },
+      { text: '支撑结构存在明显变形' },
+      { text: '立柱周边通行空间不足' },
+      { text: '立柱维护检查记录缺失' },
+    ]
+  },
+  {
+    id: 5,
+    title: '加油枪',
+    items: [
+      { text: '加油枪外观破损或变形' },
+      { text: '加油枪密封不良' },
+      { text: '加油枪工作时出现明显渗漏' },
+      { text: '汽油加油枪无集气罩' },
+      { text: '加油枪集气罩破损' },
+      { text: '枪嘴磨损或变形' },
+      { text: '加油枪开关回位不灵活' },
+      { text: '加油枪自封功能异常' },
+      { text: '加油枪挂枪座损坏' },
+      { text: '加油枪与软管连接处松动' },
+      { text: '加油枪标识不清晰' },
+      { text: '加油枪存在油污堆积' },
+      { text: '油气回收接口松动' },
+      { text: '油气回收通道堵塞' },
+      { text: '加油枪防静电连接异常' },
+      { text: '加油枪使用区域照明不足' },
+      { text: '加油枪放置位置不规范' },
+      { text: '加油枪附近存在杂物' },
+      { text: '加油枪未按要求定期维护' },
+      { text: '加油枪维修更换记录缺失' },
+    ]
+  },
+])
+
+const expandedGroupId = ref(null)
+const collapsedCaseKeys = ref([])
+const showCaseViewer = ref(false)
+const isImageCollapsed = ref(false)
+
 const selectTask = (task) => {
   currentTaskId.value = task.id
+  expandedGroupId.value = null
+  collapsedCaseKeys.value = []
 }
 
 const selectTag = (tag) => {
@@ -261,6 +447,34 @@ const toggleCheck = (index) => {
     currentTask.value.checklist[index].checked = !currentTask.value.checklist[index].checked
   }
 }
+
+const toggleGroup = (groupId) => {
+  expandedGroupId.value = expandedGroupId.value === groupId ? null : groupId
+  collapsedCaseKeys.value = []
+}
+
+const getCaseKey = (groupId, itemIndex) => `${groupId}-${itemIndex}`
+
+const isCaseCollapsed = (groupId, itemIndex) => {
+  return collapsedCaseKeys.value.includes(getCaseKey(groupId, itemIndex))
+}
+
+const toggleCase = (groupId, itemIndex) => {
+  const key = getCaseKey(groupId, itemIndex)
+  if (collapsedCaseKeys.value.includes(key)) {
+    collapsedCaseKeys.value = collapsedCaseKeys.value.filter(item => item !== key)
+  } else {
+    collapsedCaseKeys.value = [...collapsedCaseKeys.value, key]
+  }
+}
+
+const openCaseViewer = () => {
+  showCaseViewer.value = true
+}
+
+const closeCaseViewer = () => {
+  showCaseViewer.value = false
+}
 </script>
 
 <style scoped lang="scss">
@@ -423,22 +637,21 @@ const toggleCheck = (index) => {
 }
 
 .content-body {
-  display: flex;
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) 360px;
+  grid-template-rows: auto minmax(0, 1fr);
   gap: 20px;
   flex: 1;
   overflow: hidden;
 }
 
 .center-panel {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-  overflow-y: auto;
-  padding-right: 8px;
+  display: contents;
 }
 
 .image-section {
+  grid-column: 1;
+  grid-row: 1;
   position: relative;
   background: #fff;
   border-radius: 12px;
@@ -448,6 +661,61 @@ const toggleCheck = (index) => {
   background: #f5f5f5 url('/demo1.png') center center no-repeat;
   background-size: contain;
 
+  &.collapsed {
+    min-height: 72px;
+    background: #fff;
+  }
+
+  .image-toolbar {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    z-index: 2;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 6px 8px;
+    border-radius: 6px;
+    background: rgba(255, 255, 255, 0.9);
+    box-shadow: 0 1px 4px rgba(0,0,0,0.08);
+
+    span {
+      font-size: 12px;
+      color: #4b5568;
+    }
+
+    button {
+      border: 0;
+      border-radius: 4px;
+      background: #3f7cff;
+      color: #fff;
+      font-size: 12px;
+      padding: 3px 8px;
+      cursor: pointer;
+    }
+  }
+
+  .image-collapsed-summary {
+    height: 72px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px 16px;
+
+    img {
+      width: 64px;
+      height: 48px;
+      border-radius: 5px;
+      object-fit: cover;
+    }
+
+    span {
+      font-size: 14px;
+      color: #4b5568;
+      font-weight: 600;
+    }
+  }
+
   .main-image {
     width: 100%;
     height: auto;
@@ -488,11 +756,18 @@ const toggleCheck = (index) => {
 }
 
 .ai-section {
+  grid-column: 2;
+  grid-row: 1 / 3;
+  align-self: stretch;
   background: #fff;
   border-radius: 12px;
   padding: 16px;
   box-shadow: 0 1px 4px rgba(0,0,0,0.05);
-  flex-shrink: 0;
+  min-height: 0;
+  height: auto;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
 
   .section-header {
     display: flex;
@@ -509,6 +784,9 @@ const toggleCheck = (index) => {
   }
 
   .ai-content {
+    flex: 1;
+    overflow-y: auto;
+
     h4 {
       font-size: 14px;
       font-weight: 600;
@@ -652,8 +930,11 @@ const toggleCheck = (index) => {
 }
 
 .right-panel {
-  width: 360px;
-  flex-shrink: 0;
+  grid-column: 1;
+  grid-row: 2;
+  min-width: 0;
+  min-height: 0;
+  overflow: hidden;
 }
 
 .panel-section {
@@ -662,6 +943,7 @@ const toggleCheck = (index) => {
   padding: 16px;
   box-shadow: 0 1px 4px rgba(0,0,0,0.05);
   height: 100%;
+  min-height: 0;
   display: flex;
   flex-direction: column;
   overflow: hidden;
@@ -670,48 +952,339 @@ const toggleCheck = (index) => {
     display: flex;
     justify-content: space-between;
     align-items: center;
-    margin-bottom: 12px;
+    margin-bottom: 16px;
     flex-shrink: 0;
 
     .section-title {
-      font-size: 16px;
-      font-weight: 500;
+      font-size: 18px;
+      font-weight: 700;
       color: #333;
     }
+
+    .section-count {
+      width: 28px;
+      height: 28px;
+      border-radius: 50%;
+      background: #3f7cff;
+      color: #fff;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 15px;
+      font-weight: 700;
+    }
+
   }
 
   .checklist {
     flex: 1;
     overflow-y: auto;
+    padding-right: 4px;
 
-    .checklist-item {
+    .check-group {
+      border: 1px solid #d9dee8;
+      border-radius: 8px;
+      overflow: hidden;
+      background: #f8fafc;
+      margin-bottom: 10px;
+
+      &.expanded {
+        border-color: #6c79ff;
+        background: #fff;
+
+        .check-group-header {
+          background: #efedff;
+        }
+
+        .group-caret {
+          transform: rotate(90deg);
+        }
+      }
+    }
+
+    .check-group-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+      min-height: 48px;
+      padding: 0 14px;
+      border: 0;
+      background: #f8fafc;
+      color: #26324d;
+      font-size: 16px;
+      font-weight: 700;
+      cursor: pointer;
+    }
+
+    .check-group-title,
+    .group-meta {
       display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .group-index {
+      width: 22px;
+      height: 22px;
+      border-radius: 50%;
+      background: #2d5ac5;
+      color: #fff;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 13px;
+      font-weight: 700;
+    }
+
+    .group-meta {
+      color: #7a8494;
+      font-size: 14px;
+      font-weight: 500;
+    }
+
+    .group-caret {
+      font-size: 12px;
+      color: #a6afbd;
+      transition: transform 0.2s ease;
+    }
+
+    .checklist-row {
+      &:first-of-type {
+        .checklist-item {
+          margin-top: 10px;
+        }
+      }
+    }
+
+    .checklist-item {
+      display: grid;
+      grid-template-columns: 22px minmax(0, 1fr) 14px;
+      gap: 10px;
       align-items: flex-start;
-      padding: 10px 0;
-      border-bottom: 1px solid #f5f5f5;
+      width: calc(100% - 28px);
+      margin: 0 14px;
+      padding: 9px 14px;
+      border: 0;
+      border-radius: 7px;
+      background: #fff;
+      text-align: left;
+      cursor: pointer;
+
+      &:focus,
+      &:focus-visible,
+      &:active {
+        outline: none;
+        box-shadow: none;
+      }
 
-      &:last-child {
-        border-bottom: none;
+      &.collapsed {
+        .item-caret {
+          transform: rotate(-90deg);
+        }
+      }
+
+      .item-index {
+        width: 22px;
+        height: 22px;
+        border-radius: 50%;
+        background: #e2e7ef;
+        color: #7f8da3;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 12px;
+        font-weight: 700;
+        line-height: 1;
+        margin-top: 1px;
       }
 
-      input {
-        margin-right: 12px;
-        margin-top: 2px;
-        cursor: pointer;
-        flex-shrink: 0;
+      .item-text {
+        font-size: 14px;
+        color: #4b5568;
+        line-height: 1.55;
+      }
+
+      .item-caret {
+        color: #a6afbd;
+        font-size: 12px;
+        line-height: 22px;
+        transition: transform 0.2s ease;
+      }
+    }
+
+    .case-examples {
+      position: relative;
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 12px;
+      margin: 4px 14px 12px 46px;
+      padding: 12px;
+      border-radius: 8px;
+      background: #f8fafc;
+      border: 1px solid #e7ecf3;
+    }
+
+    .case-view-btn {
+      position: absolute;
+      top: 10px;
+      right: 10px;
+      z-index: 1;
+      padding: 4px 10px;
+      border: 1px solid #3f7cff;
+      border-radius: 4px;
+      background: rgba(255, 255, 255, 0.92);
+      color: #3f7cff;
+      font-size: 12px;
+      cursor: pointer;
+
+      &:focus,
+      &:focus-visible,
+      &:active {
+        outline: none;
+        box-shadow: none;
       }
+    }
+
+    .case-card {
+      min-width: 0;
 
-      span {
+      .case-title {
         font-size: 13px;
-        color: #666;
-        line-height: 1.5;
+        font-weight: 700;
+        margin-bottom: 8px;
+      }
+
+      img {
+        width: 100%;
+        height: 220px;
+        display: block;
+        object-fit: contain;
+        border-radius: 6px;
+        border: 1px solid #e5eaf0;
+        background: #f6f8fb;
       }
 
-      &.checked span {
-        text-decoration: line-through;
-        color: #999;
+      &.positive .case-title {
+        color: #16a34a;
+      }
+
+      &.negative .case-title {
+        color: #dc2626;
       }
     }
+
+    @media (max-width: 1200px) {
+      .case-examples {
+        grid-template-columns: 1fr;
+      }
+    }
+  }
+}
+
+.case-viewer {
+  position: fixed;
+  inset: 0;
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 28px;
+  background: rgba(12, 20, 38, 0.72);
+}
+
+.case-viewer-panel {
+  width: min(1500px, 96vw);
+  height: min(860px, 92vh);
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.case-viewer-header {
+  height: 54px;
+  padding: 0 18px 0 22px;
+  border-bottom: 1px solid #edf0f5;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 18px;
+  font-weight: 700;
+  color: #1f2937;
+  flex-shrink: 0;
+}
+
+.case-viewer-close {
+  width: 34px;
+  height: 34px;
+  border: 0;
+  border-radius: 50%;
+  background: #f3f6fb;
+  color: #4b5568;
+  font-size: 24px;
+  line-height: 1;
+  cursor: pointer;
+
+  &:hover {
+    background: #e7ecf3;
+  }
+
+  &:focus,
+  &:focus-visible,
+  &:active {
+    outline: none;
+    box-shadow: none;
+  }
+}
+
+.case-viewer-body {
+  flex: 1;
+  min-height: 0;
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 18px;
+  padding: 18px;
+  background: #f6f8fb;
+}
+
+.case-viewer-card {
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+
+  .case-viewer-title {
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  img {
+    flex: 1;
+    min-height: 0;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    border-radius: 8px;
+    border: 1px solid #e2e8f0;
+    background: #fff;
+  }
+
+  &.positive .case-viewer-title {
+    color: #16a34a;
+  }
+
+  &.negative .case-viewer-title {
+    color: #dc2626;
+  }
+}
+
+@media (max-width: 900px) {
+  .case-viewer-body {
+    grid-template-columns: 1fr;
   }
 }
+
 </style>