Riku
2025-06-23 ff82e86becbd200adabd2ce56fba1f6b3c6c37e1
2025.6.23
已修改11个文件
已添加4个文件
1490 ■■■■ 文件已修改
src/api/fysp/deviceApi.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fysp/mediafileApi.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BaseCard.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FYImageSelectDialog.vue 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/inspection/ProblemItem.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/inspection/SceneDevice.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/inspection/TaskItem.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/main/MonitorView.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management/TaskStats.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/management/TaskSummary.vue 102 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/visualization/SubtaskVisual.vue 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/visualization/SupervisionVisual.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fysp/deviceApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
import { $fysp } from '../index';
export default {
  // èŽ·å–è®¾å¤‡
  fetchDevices(sceneId, deviceTypeId) {
    const params = `?sceneId=${sceneId}&deviceTypeId=${deviceTypeId}`;
    return $fysp
      .get(`device${params}`)
      .then((res) => res)
      .then((res) => res.data);
  },
  // èŽ·å–è®¾å¤‡çŠ¶æ€ä»¥åŠè®¾å¤‡è¯¦æƒ…
  fetchDeviceStatus({ deviceId, sceneId, deviceTypeId }) {
    const params = `?deviceId=${deviceId}&sceneId=${sceneId}&deviceTypeId=${deviceTypeId}`;
    return $fysp
      .get(`device/status${params}`)
      .then((res) => res)
      .then((res) => res.data);
  }
};
src/api/fysp/mediafileApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import { $fysp } from '../index';
export default {
  /**
   * èŽ·å–å·¡æŸ¥ä¸­çš„ä»»æ„å›¾ç‰‡
   */
  getRoutineByStGuid(stGuid) {
    const params = `?stGuid=${stGuid}`;
    return $fysp.get(`mediafile/routine${params}`).then((res) => res.data);
  },
  getRoutineByiGuid(iGuid) {
    const params = `?iGuid=${iGuid}`;
    return $fysp.get(`mediafile/routine${params}`).then((res) => res.data);
  },
  updateMediaFile(mediafile) {
    return $fysp.post('mediafile', mediafile).then((res) => res.data);
  }
};
src/api/index.js
@@ -41,11 +41,11 @@
  i.interceptors.request.use(
    function (config) {
      // åœ¨å‘送请求之前做些什么
      console.log('==>请求开始')
      console.log(`${config.baseURL}${config.url}`)
      if (config.data) {
        console.log('==>请求数据', config.data)
      }
      // console.log('==>请求开始')
      // console.log(`${config.baseURL}${config.url}`)
      // if (config.data) {
      //   console.log('==>请求数据', config.data)
      // }
      return config
    },
    function (error) {
@@ -65,6 +65,11 @@
    function (response) {
      // 2xx èŒƒå›´å†…的状态码都会触发该函数。
      // å¯¹å“åº”数据做点什么
      console.log('==>请求开始')
      console.log(`${response.config.baseURL}${response.config.url}`)
      if (response.config.data) {
        console.log('==>请求数据', response.config.data)
      }
      console.log(response)
      console.log('==>请求结束')
      if (response.status == 200) {
src/components.d.ts
@@ -19,6 +19,7 @@
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElEmpty: typeof import('element-plus/es')['ElEmpty']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElImage: typeof import('element-plus/es')['ElImage']
    ElLink: typeof import('element-plus/es')['ElLink']
@@ -32,14 +33,18 @@
    ElStatistic: typeof import('element-plus/es')['ElStatistic']
    ElTable: typeof import('element-plus/es')['ElTable']
    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
    ElTabPane: typeof import('element-plus/es')['ElTabPane']
    ElTabs: typeof import('element-plus/es')['ElTabs']
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    FYImageSelectDialog: typeof import('./components/FYImageSelectDialog.vue')['default']
    OptionLocation: typeof import('./components/search/OptionLocation.vue')['default']
    OptionSceneType: typeof import('./components/search/OptionSceneType.vue')['default']
    OptionTime: typeof import('./components/search/OptionTime.vue')['default']
    ProblemItem: typeof import('./components/inspection/ProblemItem.vue')['default']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    SceneDevice: typeof import('./components/inspection/SceneDevice.vue')['default']
    SubtaskExamineItem: typeof import('./components/inspection/SubtaskExamineItem.vue')['default']
    SubtaskItem: typeof import('./components/inspection/SubtaskItem.vue')['default']
    TaskItem: typeof import('./components/inspection/TaskItem.vue')['default']
src/components/BaseCard.vue
@@ -20,7 +20,7 @@
      </el-row>
    </el-space>
    <!-- <Transition name="el-zoom-in-left"> -->
    <div class="m-t-8" v-if="show">
    <div class="m-t-8" v-show="show">
      <slot></slot>
    </div>
    <!-- </Transition> -->
src/components/FYImageSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,304 @@
<template>
  <!-- <el-dialog
    :model-value="dialogVisible"
    @opened="handleOpen"
    @closed="handleClose"
    top="5vh"
    width="68%"
    destroy-on-close
    :close-on-press-escape="false"
  > -->
  <!-- <el-row justify="end">
      <el-text v-if="onContextMenu != undefined" size="small" type="info">{{
        `(${contextMenuStr})`
      }}</el-text>
      <div v-if="!readonly">
        <el-text size="small" type="info" class="m-r-8"
          >最多选择{{ maxSelect }}张图片</el-text
        >
        <el-button
          size="small"
          type="primary"
          @click="handleSubmit"
          :disabled="selectedImgUrlList.length == 0"
          >确定</el-button
        >
        <el-button size="small" type="primary" @click="handleCancel"
          >取消</el-button
        >
      </div>
    </el-row> -->
  <div class="center">
    <el-tabs v-if="typeList.length > 0" v-model="activeId" type="card">
      <el-tab-pane
        v-for="item in typeList"
        :key="item.typeId"
        :label="item.typeName + ' (' + typeImgMap.get(item.typeId).length + ')'"
        :name="item.typeId"
      >
      </el-tab-pane>
    </el-tabs>
    <el-scrollbar :height="height">
      <div v-if="typeImgMap.get(activeId) && typeImgMap.get(activeId).length > 0" class="imgs">
        <el-image
          v-loading="img.loading"
          v-for="(img, i) in typeImgMap.get(activeId)"
          :key="i"
          :class="[img.isSelect ? 'selected' : 'noActive', 'image']"
          :style="`width: ${imageWidth}px;height: ${imageWidth}px;`"
          fit="cover"
          :src="img.url"
          :preview-src-list="readonly ? typeImgMap.get(activeId).map((v) => v.url) : []"
          :initial-index="i"
          @contextmenu="(e) => showContextMenu(e, i)"
          @click="onSelect(img, i)"
          @load="onOneImgLoadSuccess(img)"
          @error="onOneImgLoadError(img)"
        />
      </div>
      <el-row v-else justify="space-between">
        <el-empty description="暂无记录" />
      </el-row>
    </el-scrollbar>
  </div>
  <!-- </el-dialog> -->
</template>
<script setup>
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
  dialogVisible: Boolean,
  /**
   * å›¾ç‰‡åˆ†ç±»
   * ç»“æž„{ typeId, typeName }
   */
  typeList: {
    type: Array,
    default: () => []
  },
  typeImgMap: {
    type: Array,
    default: () => new Map()
  },
  // æ˜¯å¦ä»¥åªè¯»çš„形式查看当前页面
  readonly: {
    type: Boolean,
    default: false
  },
  // å›¾ç‰‡å¯é€‰æ•°é‡ï¼Œå½“传入数字时,代表图片数量
  maxSelect: {
    type: Number,
    default: 3
  },
  // å›¾ç‰‡å³é”®ç‚¹å‡»äº‹ä»¶
  onContextMenu: {
    type: Function
  },
  contextMenuStr: {
    type: String,
    default: '右键点击图片触发额外操作'
  },
  height: {
    type: String,
    default: '70vh'
  },
  imageWidth: {
    type: Number,
    default: 240
  }
})
const emit = defineEmits(['submit', 'cancel', 'update:dialogVisible'])
const activeId = ref('')
const selectedImgUrlList = ref([])
let loadedImgCount = ref(0)
// åŠ è½½çŠ¶æ€
const loading = computed(() => {
  if (activeId.value == '') {
    return false
  }
  // ä¿è¯æœ€å¼€å§‹æ˜¯åŠ è½½çŠ¶æ€ï¼Œä¸‰åˆ†ä¹‹ä¸€åŠ è½½ä¹‹åŽåœæ­¢å±•ç¤ºåŠ è½½çŠ¶æ€
  return !(props.typeImgMap.get(activeId.value).length / 3 <= loadedImgCount.value)
})
function onOneImgLoadError(img) {
  img.loading = false
  loadedImgCount.value++
}
function onOneImgLoadSuccess(img) {
  img.loading = false
  loadedImgCount.value++
}
watch(
  () => activeId.value,
  (nV, oV) => {
    loadedImgCount.value = 0
  },
  { immediate: true }
)
function onSelect(img, i) {
  if (props.readonly) {
    return
  }
  const imgList = selectedImgUrlList.value
  const index = imgList.indexOf(img)
  if (index == -1) {
    if (props.maxSelect == 1) {
      img.isSelect = true
      imgList.push(img)
      if (imgList.length > 1) {
        imgList.splice(0, 1).forEach((e) => {
          e.isSelect = false
        })
      }
    } else if (props.maxSelect > 1) {
      if (imgList.length < props.maxSelect) {
        img.isSelect = true
        imgList.push(img)
      }
    }
  } else {
    imgList.splice(index, 1)
    img.isSelect = false
  }
}
function handleOpen() {
  emit('update:dialogVisible', true)
}
function handleClose() {
  selectedImgUrlList.value.forEach((item) => (item.isSelect = false))
  selectedImgUrlList.value = []
  emit('update:dialogVisible', false)
}
function handleSubmit() {
  emit('submit', selectedImgUrlList.value)
  emit('update:dialogVisible', false)
}
function handleCancel() {
  emit('cancel')
  emit('update:dialogVisible', false)
}
// å›¾ç‰‡å³é”®ç‚¹å‡»æ—¶é—´
function showContextMenu(event, index) {
  if (props.onContextMenu) {
    event.preventDefault()
    props.onContextMenu(event, activeId.value, index)
  }
}
watch(
  () => props.typeList,
  (nV, oV) => {
    if (nV != oV && nV.length > 0) {
      activeId.value = nV[0].typeId
    }
  },
  { immediate: true }
)
</script>
<style scoped>
.center {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.text {
  padding: 20px;
}
.main {
  /* ä½¿çˆ¶å…ƒç´ å±…中 */
  /* margin: 0 auto;  */
  /* width: 100%; */
}
.imgs {
  width: 100%;
  /* border-style:solid;
    border-radius: 1px; */
  /* height: 100%; */
  flex-grow: 1 !important;
  overflow-y: auto !important;
  /* å†…容的内边距 */
  display: flex !important;
  flex-wrap: wrap !important;
  /* overflow: hidden; */
}
.image {
  margin: 5px;
  /* height: 250px;
  width: 240px; */
  border-radius: 4px;
}
.selected {
  margin: 3px;
  color: #4abe84;
  box-shadow: 0 2px 7px 0 rgba(85, 110, 97, 0.35);
  border: 2px solid rgba(74, 190, 132, 1);
}
.selected:before {
  content: '';
  position: absolute;
  right: 0;
  bottom: 0;
  border: 17px solid #4abe84;
  border-top-color: transparent;
  border-left-color: transparent;
}
.selected:after {
  content: '';
  width: 5px;
  height: 12px;
  position: absolute;
  right: 6px;
  bottom: 6px;
  border: 2px solid #fff;
  border-top-color: transparent;
  border-left-color: transparent;
  transform: rotate(45deg);
}
.noActive {
  /* padding: 5px; */
}
.blurry {
  filter: blur(3px);
}
.filters {
  display: flex;
  padding: 5px;
}
::v-deep .el-dialog__body {
  padding: 10px calc(var(--el-dialog-padding-primary) + 10px) !important;
}
:deep(.el-tabs__item) {
  /* color: var(--el-text-color-info); */
}
:deep(.el-tabs__item.is-active) {
  color: var(--el-color-warning);
}
:deep(.el-tabs--card > .el-tabs__header .el-tabs__nav) {
  border: 1px solid rgba(255, 255, 255, 0.541);
}
:deep(.el-tabs--card > .el-tabs__header .el-tabs__item) {
  border-left: 1px solid rgba(255, 255, 255, 0.541);
}
:deep(.el-tabs--card > .el-tabs__header .el-tabs__item:first-child) {
  border-left: none;
}
</style>
src/components/inspection/ProblemItem.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <el-text tag="b" type="warning" size="small">{{ index }}. </el-text>
    <el-text type="warning" size="small">{{ title }}</el-text>
    <el-text tag="b" size="large">{{ index }}. </el-text>
    <el-text size="large">{{ title }}</el-text>
  </div>
  <!-- <div>
    <el-text>{{ proStatus.name }}</el-text>
@@ -9,7 +9,7 @@
  <template v-for="(pic, t) in pics" :key="t">
    <template v-if="pic.path.length > 0">
      <div>
        <el-text size="small" type="info">{{ pic.title }}</el-text>
        <el-text size="default" type="info">{{ pic.title }}</el-text>
      </div>
      <el-space>
        <el-image
@@ -61,8 +61,8 @@
</script>
<style scoped>
.image {
  width: 60px;
  height: 60px;
  width: 134px;
  height: 134px;
  border-radius: 2px;
}
</style>
src/components/inspection/SceneDevice.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <!-- <CompGenericWrapper type="drawer">
    <template #content> -->
  <!-- é€‰é¡¹ -->
  <!-- è®¾å¤‡ç±»åž‹  -->
  <el-row>
    <el-col>
      <el-tabs class="child_select" placeholder="设备类型" v-model="currSelect.topDeviceTypeId">
        <el-tab-pane v-for="item in deviceTopTypes" :key="item.id" :name="item.id">
          <template #label>
            <el-badge :value="item.count" :type="item.count == 0 ? 'danger' : 'primary'">
              <span class="custom-tabs-label">
                <span>{{ item.label }}</span>
              </span>
            </el-badge>
          </template>
        </el-tab-pane>
      </el-tabs>
    </el-col>
  </el-row>
  <el-collapse v-model="activeNames" style="border: 4px">
    <el-collapse-item
      v-for="item in formInfo"
      :key="item.id"
      :name="item.id"
      class="collapse-item-class"
    >
      <template #title>
        <div style="display: flex; width: 100%; justify-content: space-between">
          <div style="">
            <el-descriptions style="" :column="3" size="small" border>
              <el-descriptions-item
                width="64px"
                :label="currSelect.topDeviceTypeId == 0 ? '站点名称' : '设备名称'"
                :span="3"
                >{{ item.name || '无' }}</el-descriptions-item
              >
              <el-descriptions-item label="供应商">{{
                item.supplier || '无'
              }}</el-descriptions-item>
              <el-descriptions-item label="运维商">{{
                item.maintainer || '无'
              }}</el-descriptions-item>
              <el-descriptions-item label="运维频次">
                <el-select
                  v-model="item.maintainFrequency"
                  :disabled="isDisabled"
                  style="width: 150px"
                >
                  <el-option
                    v-for="frequency of maintainFrequencysArray"
                    :key="frequency.key"
                    :label="frequency.value"
                    :value="frequency.key"
                  ></el-option>
                </el-select>
              </el-descriptions-item>
              <el-descriptions-item label="运维人员">{{
                item.maintainStaff || '无'
              }}</el-descriptions-item>
              <el-descriptions-item label="运维联系方式">{{
                item.maintainTel || '无'
              }}</el-descriptions-item>
              <el-descriptions-item label="品牌型号">{{
                item.brandModel || '无'
              }}</el-descriptions-item>
              <el-descriptions-item label="运行状态">
                <el-select v-model="item.runningStatus" :disabled="isDisabled" style="width: 150px">
                  <el-option
                    v-for="status of runStatusArray"
                    :key="status.key"
                    :label="status.value"
                    :value="status.key"
                  ></el-option>
                </el-select>
              </el-descriptions-item>
              <el-descriptions-item label="类型">
                {{ item._typename || '无' }}
              </el-descriptions-item>
            </el-descriptions>
          </div>
          <div style="display: flex">
            <!-- <div class="sub-title">{{ item.name }}</div> -->
            <!-- å›¾ç‰‡ -->
            <div class="image-container">
              <div
                class="block-div"
                @click="onClickPic($event)"
                v-for="(status, index) in item._statusList"
                :key="index"
              >
                <el-image
                  v-if="index == 0"
                  fit="cover"
                  class="pic-style"
                  :src="status._picUrl"
                  :preview-src-list="Array.of(status._picUrl)"
                />
                <span class="abstract_pic_text" v-if="index == 0">{{
                  `最新状态图片 ${status.dlCreateTime.slice(0, 10)}`
                }}</span>
              </div>
            </div>
          </div>
        </div>
      </template>
      <!-- è¯¦ç»†å†…容开始 -->
      <el-form :model="item" class="form_class">
        <el-form-item label="状态">
          <el-tabs tab-position="top">
            <el-tab-pane
              v-for="(status, i) in item._statusList"
              :label="status.dlCreateTime.slice(0, 10)"
              :key="i"
            >
              <el-form :model="status" class="form-class">
                <el-form-item label="位置" style="margin-bottom: 10px">
                  {{ status.dlLocation }}
                </el-form-item>
                <el-form-item label="图片">
                  <!-- å›¾ç‰‡ -->
                  <el-space>
                    <div v-if="status._paths && status._paths.length > 0">
                      <el-image
                        v-for="(path, i) in status._paths"
                        fit="cover"
                        class="pic-style"
                        :src="path"
                        :preview-src-list="Array.of(path)"
                        :key="i"
                      />
                    </div>
                    <el-empty v-else></el-empty>
                  </el-space>
                </el-form-item>
              </el-form>
            </el-tab-pane>
          </el-tabs>
        </el-form-item>
      </el-form>
      <!-- è¯¦ç»†å†…容结束 -->
    </el-collapse-item>
  </el-collapse>
  <!-- ç©ºçŠ¶æ€ -->
  <el-empty v-if="isEmpty" />
  <!-- </template>
  </CompGenericWrapper> -->
</template>
<script>
import deviceApi from '@/api/fysp/deviceApi'
import { $fysp } from '@/api/index'
import { toLabel } from '@/enum/device/device'
export default {
  components: {},
  props: {
    scene: Object
  },
  watch: {
    // é€‰æ‹©æ”¹å˜ç›‘听
    currSelect: {
      handler(newObj, oldObj) {
        this.getList()
      },
      deep: true
    }
  },
  data() {
    return {
      activeNames: [],
      // æŽ§åˆ¶æ˜¯å¦å±•示空状态
      isEmpty: false,
      // è¯¦æƒ…按钮大小
      detailSize: '22px',
      // è¡¨å•详情点击按钮的图标
      isDetail: false,
      currSelect: {
        topDeviceTypeId: 0
      },
      // æŽ§åˆ¶è¡¨å•是否可以编辑
      isDisabled: true,
      formInfo: {},
      rules: [],
      // è®¾å¤‡ç±»åž‹
      deviceTopTypes: [
        { id: 0, label: '监控设备' },
        { id: 1, label: '治理设备' },
        { id: 2, label: '生产设备' }
      ],
      // è¿è¡ŒçŠ¶æ€
      runStatusArray: [
        { key: 0, value: '未联网' },
        { key: 1, value: '上线中' },
        { key: 2, value: '下线' },
        { key: 3, value: '拆除' }
      ],
      // ç»´æŠ¤é¢‘率状态
      maintainFrequencysArray: [
        { key: 1, value: '每月一次' },
        { key: 2, value: '每季度一次' },
        { key: 3, value: '每半年一次' },
        { key: 4, value: '每年一次' }
      ],
      // ç§Ÿèµæ–¹å¼
      ownershipArray: [
        { key: 0, value: 'è´­ä¹°' },
        { key: 1, value: '租赁' }
      ],
      scene: {}
    }
  },
  mounted() {},
  methods: {
    // èŽ·å–å½“å‰ç±»åž‹è®¾å¤‡æ•°é‡
    getTabsCount() {
      this.deviceTopTypes.forEach((item) => {
        deviceApi.fetchDevices(this.scene.guid, item.id).then((result) => {
          item.count = result.data.length
        })
      })
    },
    // èŽ·å–è¿è¡ŒçŠ¶æ€å¯¹åº”çš„value
    getRunStatusValueByRunStatusKey(status) {
      var runningStatusValueArray = this.runStatusArray.filter((runStatus) => {
        return runStatus.key == status
      })
      if (runningStatusValueArray.length > 0) {
        return runningStatusValueArray[0].value
      }
    },
    // å±•示表单的详情的点击事件
    showDetail(item) {
      item._isDetail = !item._isDetail
    },
    init(scene) {
      // çˆ¶ç»„件主动调用初始化子组件的方法
      this.scene = scene
      this.getList()
      this.getTabsCount()
    },
    // é‡ç½®å±•示的数据
    initList() {
      this.formInfo = []
      this.isEmpty = false
    },
    // æ ‡å‡†åŒ–属性名
    convertKeys(obj) {
      // å°†ä¸€ä¸ªjs对象中所有di,wi,pi开头的属性全部改成去掉这些前缀并且重新变为驼峰式命名
      const newObj = {}
      for (const key in obj) {
        let newKey = key
        if (key.startsWith('di')) {
          newKey = key.substring(2)
        } else if (key.startsWith('wi')) {
          newKey = key.substring(2)
        } else if (key.startsWith('pi')) {
          newKey = key.substring(2)
        }
        newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1)
        newObj[newKey] = obj[key]
      }
      return newObj
    },
    // æ–°å¢žå­—段
    initFormData(data) {
      data._isDetail = false
    },
    getList() {
      deviceApi.fetchDevices(this.scene.guid, this.currSelect.topDeviceTypeId).then((result) => {
        this.initList()
        if (result.data == null || result.data.length <= 0) {
          this.isEmpty = true
          return
        }
        // æ ‡å‡†åŒ–属性名
        for (let index = 0; index < result.data.length; index++) {
          var element = this.convertKeys(result.data[index])
          this.initFormData(element)
          // èŽ·å–è®¾å¤‡çŠ¶æ€ä¿¡æ¯
          let data = {
            deviceId: element.id,
            sceneId: element.sceneGuid,
            deviceTypeId: this.currSelect.topDeviceTypeId
          }
          deviceApi.fetchDeviceStatus(data).then((status) => {
            var statusData = status.data
            var imgPaths = []
            if (statusData) {
              if (statusData.length == 0) {
                this.formInfo.push(element)
                return
              }
              element = this.convertKeys(result.data[index])
              element = this.setDeviceType(element)
              element._picUrls = imgPaths
              for (let index = 0; index < statusData.length; index++) {
                const statusItem = statusData[index]
                // è®¾å¤‡å¯¹è±¡æ·»åŠ ä¸€ä¸ªå±žæ€§åˆ—è¡¨å±žæ€§ç”¨æ¥ä¿å­˜è®¾å¤‡çŠ¶æ€
                this.saveStatus(element, statusItem)
                element.dlLocation = statusItem.dlLocation
                this.formInfo.push(element)
              }
            }
          })
        }
      })
    },
    setDeviceType(element) {
      var type = []
      type = toLabel(element.sceneTypeId, this.currSelect.topDeviceTypeId, [
        element.typeId,
        element.subtypeId
      ])
      element._typename = type.join('-')
      return element
    },
    // ä¿å­˜çŠ¶æ€ä¿¡æ¯
    saveStatus(device, status) {
      var _picUrl = $fysp.imgUrl + status.dlPicUrl
      status._picUrl = _picUrl
      status._paths = _picUrl.split(';')
      device._picUrls.push(_picUrl)
      if ('_statusList' in device) {
        device._statusList.push(status)
      } else {
        device._statusList = Array.of(status)
      }
      // æŽ’序
      device._statusList.sort(function (x, y) {
        return new Date(x.dlCreateTime) - new Date(y.dlCreateTime) //    é™åºï¼Œå‡åºåˆ™åä¹‹
      })
    },
    submit() {},
    cancel() {},
    modifyObjectKeys(obj) {
      const newObj = {}
      for (const key in obj) {
        // è·³è¿‡ä»¥ 'dl' æˆ– '_' å¼€å¤´çš„属性
        if (key.startsWith('dl') || key.startsWith('_')) {
          newObj[key] = obj[key]
          continue
        }
        // æ ¹æ® topDeviceTypeId æ·»åŠ å‰ç¼€
        let prefix = ''
        switch (this.currSelect.topDeviceTypeId) {
          case 0:
            prefix = 'di'
            break
          case 1:
            prefix = 'pi'
            break
          case 2:
            prefix = 'wi'
            break
          default:
            // å¦‚æžœ topDeviceTypeId ä¸æ˜¯ 0, 1, æˆ– 2,不添加前缀
            newObj[key] = obj[key]
            continue
        }
        // æ·»åŠ å‰ç¼€å¹¶è½¬æ¢ä¸ºé©¼å³°å¼å‘½å
        const newKey = `${prefix}${key.charAt(0).toUpperCase() + key.slice(1)}`
        newObj[newKey] = obj[key]
      }
      return newObj
    },
    // ç”ŸæˆæŽ¥å£å‚æ•°
    generateQuery(obj) {
      // éœ€è¦æ ¹æ®åœºæ™¯ç±»åž‹ç¡®å®šæŽ¥å£å‚数的属性名
      var query = this.modifyObjectKeys(obj)
      return query
    },
    onClickPic(e, item) {
      e.stopPropagation()
    }
  }
}
</script>
<style scoped>
.image-container {
  justify-content: flex-end;
  display: flex;
  /* width: 300px; */
  /* flex-direction: row-reverse; */
  /* height: 225px; */
  /* overflow: hidden; ç¡®ä¿å›¾ç‰‡ä¸ä¼šè¶…出容器 */
}
.pic-style {
  width: 150px;
  height: 150px;
  border-radius: 4px;
}
.card-style {
  height: 400px;
  margin-bottom: 10px;
  border-color: rgba(0, 0, 0, 0.308);
}
.centerDiv {
  text-align: center; /* æ°´å¹³å±…中 */
}
.dot {
  position: absolute;
  top: 0;
  right: 0;
  width: 10px;
  height: 10px;
  background-color: #f56c6c;
  border-radius: 50%;
}
.abstract_main {
  width: 98%;
}
.abstract_main_item {
  display: flex;
  flex-direction: column;
  margin-right: 50px;
  margin-top: 10px;
  /* width: 20%; */
}
.abstract_other_item {
  /* display: flex;
    flex-direction: column; */
  /* margin-left: 50px; */
  /* margin-top: 10px;
    width: 100vh; */
}
.abstract_main_item_inner {
  display: flex;
  justify-content: center;
}
.abstract_other_item_inner {
  margin-left: 10px;
  display: flex;
}
.abstract_main_title {
  /* margin-left: -400px; */
  color: #303133;
  font-size: 16px;
}
.abstract_main_title {
  color: #606266;
  font-size: 13px;
  margin-top: 10px;
}
.abstract_other_title {
  color: #606266;
  font-size: 13px;
  margin-top: 45px;
}
.abstract_main_text {
  color: #303133;
  font-size: 17px;
  margin-top: 5px;
}
.abstract_pic_text {
  display: block;
  color: var(--el-text-color-secondary);
  font-size: 14px;
  /* margin-top: 20px; */
}
.block-div {
  display: block;
}
.form_class {
  /* margin-left: 10px; */
}
.el-collapse {
  /* æŠ˜å é¢æ¿æŠ˜å æ—¶çš„高度 */
  --el-collapse-header-height: auto;
}
.el-collapse-item__header {
  width: 100%;
}
.form-class {
  width: 50vw;
}
.form-item-class {
  margin-bottom: 10px;
}
.sub-title {
  font-size: var(--el-font-size-large);
  margin-bottom: 30px;
  margin-left: 20px;
}
.collapse-item-class {
  height: 100%;
  border: 5px;
}
::-webkit-scrollbar {
  height: 0;
}
</style>
src/components/inspection/TaskItem.vue
@@ -1,43 +1,69 @@
<template>
  <div v-if="value">
    <el-row justify="start">
      <el-text>{{ value.name }}</el-text>
  <div v-if="value" class="wrapper">
    <el-row justify="center">
      <el-text size="large">{{ value.districtName }}</el-text>
      <!-- <el-tag type="info">{{ value.districtName }}</el-tag> -->
    </el-row>
    <div>
      <el-text>总量</el-text>
      <el-text v-if="value._totaltask > 0" size="default">{{
        value._completetask + '/' + value._totaltask
      }}</el-text>
      <el-text v-else size="default">{{ value.completetask + '/' + value.totaltask }}</el-text>
      <div class="text_title">实施进度</div>
      <template v-if="value._totaltask > 0">
        <el-space>
          <!-- <el-text size="default">{{ value._completetask + '/' + value._totaltask }}</el-text> -->
          <el-text>总计</el-text>
      <el-progress
        style="width: 300px"
            style="width: 350px"
        type="line"
        status="warning"
        :text-inside="true"
        :stroke-width="18"
            :stroke-width="22"
            :striped="percentFormat(value._completetask, value._totaltask) < 100"
            striped-flow
            :percentage="percentFormat(value._completetask, value._totaltask)"
          >
            <template #default="{ percentage }">
              <span class="percentage-value">{{
                `${value._completetask}/${value._totaltask} (${percentage}%)`
              }}</span>
            </template>
          </el-progress>
        </el-space>
      </template>
      <template v-else>
        <el-space>
          <!-- <el-text size="default">{{ value.completetask + '/' + value.totaltask }}</el-text> -->
          <el-text>总计</el-text>
          <el-progress
            style="width: 350px"
            type="line"
            status="warning"
            :text-inside="true"
            :stroke-width="22"
        :striped="percentFormat(value.completetask, value.totaltask) < 100"
        striped-flow
        :percentage="percentFormat(value.completetask, value.totaltask)"
      >
        <template #default="{ percentage }">
          <span class="percentage-value">{{ percentage }}%</span>
              <span class="percentage-value">{{
                `${value.completetask}/${value.totaltask} (${percentage}%)`
              }}</span>
        </template>
      </el-progress>
        </el-space>
      </template>
    </div>
    <!-- </el-col> -->
    <!-- <el-col span="12" class="flex-bottom"> -->
    <!-- <div>{{ name }}</div> -->
    <!-- <div>{{ planTime }}</div> -->
    <!-- <div>{{ userName }}</div> -->
    <el-row class="m-t-8">
      <div
        align="center"
        :style="'width: ' + 300 / value.count.length + 'px'"
        v-for="item in value.count"
        :key="item.sceneType"
      >
    <!-- :style="'width: ' + 300 / value.count.length + 'px'" -->
    <!-- <el-row class="m-t-8"> -->
    <div v-for="item in value.count" :key="item.sceneType">
      <el-space v-if="item.finish > 0">
        <el-text size="default" truncated>{{ item.sceneType }}</el-text>
        <el-progress
          :stroke-width="18"
          :style="'width:' + width"
          :stroke-width="16"
          status="exception"
          :text-inside="true"
          :striped="percentFormat(item.finish, item.total) < 100"
@@ -45,72 +71,85 @@
          :percentage="percentFormat(item.finish, item.total)"
        >
          <template #default="{ percentage }">
            <span class="percentage-value-small">{{ percentage }}%</span>
            <span class="percentage-value-small">{{
              `${item.finish}/${item.total} (${percentage}%)`
            }}</span>
          </template>
        </el-progress>
        <el-text size="small" truncated>{{ item.sceneType }}</el-text>
        <!-- <el-text size="small">{{ item.finish + '/' + item.total }}</el-text> -->
        <!-- <span class="percentage-value-small">{{ percentFormat(item.finish, item.total) }}%</span> -->
        <!-- <div class="percentage-label-small">{{ item.sceneType }}</div> -->
        <!-- <span class="percentage-label-small">{{ item.finish + '/' + item.total }} </span> -->
      </el-space>
      </div>
    </el-row>
    <div>
      <div class="text_title">日程进度</div>
      <el-space>
        <el-text style="color: transparent;">日程</el-text>
        <el-progress
          style="width: 350px"
          type="line"
          color="#00b487"
          :text-inside="true"
          :stroke-width="22"
          :percentage="percentFormat(date, dayCount)"
        >
          <template #default="{ percentage }">
            <span class="percentage-value">{{ `${dateStr} (${percentage}%)` }}</span>
          </template>
        </el-progress>
      </el-space>
    </div>
    <!-- </el-row> -->
  </div>
</template>
<script>
<script setup>
import { ref, computed } from 'vue'
import { useAreaStore } from '@/stores/area.js'
import dayjs from 'dayjs'
/**
 * å·¡æŸ¥ä»»åŠ¡åŒºåŸŸç»Ÿè®¡ä¿¡æ¯
 */
export default {
  props: {
    // name: String,
    // province: String,
    // district: String,
    // planTime: String,
    // startTime: String,
    // endTime: String,
    // userName: String,
    // status: String,
    // totaltask: Number,
    // completetask: Number,
    // count: Array,
const areaStore = useAreaStore()
const props = defineProps({
    value: Object
  },
  data() {
    return {}
  },
  watch: {},
  computed: {
    // total() {
    //   let t = 0
    //   this.count.forEach((c) => {
    //     t += c.total
    //   })
    //   return t
    // },
    // finish() {
    //   let t = 0
    //   this.count.forEach((c) => {
    //     t += c.finish
    //   })
    //   return t
    // }
  },
  methods: {
    percentFormat(finish, total) {
})
const width = ref('300px')
const dayCount = computed(() => dayjs(areaStore.area.endtime).daysInMonth())
const date = computed(() => {
  const today = dayjs()
  const planEndTime = dayjs(areaStore.area.endtime)
  if (today.isBefore(planEndTime)) {
    return today.date()
  } else {
    return planEndTime.daysInMonth()
  }
})
const dateStr = computed(()=>{
  const today = dayjs()
  const planEndTime = dayjs(areaStore.area.endtime)
  if (today.isBefore(planEndTime)) {
    return today.format('MM月DD日')
  } else {
    return today.format('MM月DD日')
  }
})
function percentFormat(finish, total) {
      if (total == 0) {
        return 0
      } else {
        const per = finish / total > 1 ? 1 : finish / total
        return Math.round(per * 100)
      }
    },
    format(percentage) {
}
function format(percentage) {
      percentage === 100 ? 'Full' : `${percentage}%`
    }
  }
}
</script>
@@ -118,6 +157,19 @@
.wrapper {
  border: var(--el-border);
  border-radius: var(--el-border-radius-base);
  box-shadow: var(--el-box-shadow-lighter);
  background-color: rgba(161, 161, 161, 0.068);
  padding: 8px 8px;
}
.text_title {
  border-left: 6px solid rgb(49, 221, 6);
  border-bottom: 1px solid rgb(49, 221, 6);
  padding: 0px 4px 0px 4px;
  width: 100px;
  font-size: 15px;
  margin-bottom: 4px;
  /* color: #00b487; */
}
.flex-bottom {
@@ -145,4 +197,7 @@
  /* margin-top: 10px; */
  font-size: var(--el-font-size-small);
}
:deep(.el-progress-bar__outer) {
  background-color: rgba(211, 211, 211, 0.411);
}
</style>
src/main.js
@@ -3,6 +3,11 @@
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/theme-chalk/src/dark/css-vars.scss'
import 'element-plus/theme-chalk/src/overlay.scss';
import 'element-plus/theme-chalk/src/message.scss';
import 'element-plus/theme-chalk/src/message-box.scss';
import 'element-plus/theme-chalk/src/notification.scss';
import App from './App.vue'
import router from './router'
import timeUtil from './utils/time-util'
src/views/main/MonitorView.vue
@@ -1,27 +1,31 @@
<template>
  <BaseMap></BaseMap>
  <el-row class="overlay-container" v-if="true">
    <el-col :span="17">
      <el-scrollbar class="page-left-top">
    <el-col :span="7" class="page-right">
      <el-scrollbar height="var(--fy-body-height)" class="p-events-auto" style="width: 450px">
        <TaskStats></TaskStats>
        <TaskSummary></TaskSummary>
      </el-scrollbar>
    </el-col>
    <el-col :span="10">
      <el-row justify="end">
        <SubtaskVisual class="subtask-visual"></SubtaskVisual>
      </el-row>
      <!-- <el-scrollbar class="page-left-top"> -->
        <!-- <VisualizationView></VisualizationView> -->
      </el-scrollbar>
      <el-scrollbar class="page-left-bottom p-events-auto">
        <!-- <InspectionView></InspectionView> -->
      </el-scrollbar>
      <!-- </el-scrollbar> -->
      <!-- <el-scrollbar class="page-left-bottom p-events-auto">
        <InspectionView></InspectionView>
      </el-scrollbar> -->
    </el-col>
    <el-col :span="7" class="page-right">
      <el-scrollbar height="var(--fy-body-height)" class="p-events-auto">
        <ManagementView></ManagementView>
      </el-scrollbar>
    </el-col>
    <!-- <el-col :span="7" class="page-right">
      <el-scrollbar height="var(--fy-body-height)">
        <StatisticView></StatisticView>
      </el-scrollbar>
    </el-col> -->
  </el-row>
  <SupervisionVisual class="supervision-view"></SupervisionVisual>
  <TaskStats class="task-stats"></TaskStats>
  <WorkStream class="work-stream"></WorkStream>
  <!-- <ProblemTrack class="problem-track"></ProblemTrack> -->
</template>
@@ -47,6 +51,8 @@
import SupervisionVisual from '@/views/visualization/SupervisionVisual.vue'
import WorkStream from '@/views/inspection/WorkStream.vue'
import TaskStats from '@/views/management/TaskStats.vue'
import TaskSummary from '@/views/management/TaskSummary.vue'
import SubtaskVisual from '@/views/visualization/SubtaskVisual.vue'
// provide('mapHeight', 'calc(var(--fy-body-height) / 4 * 3)')
provide('mapHeight', 'calc(var(--fy-body-height))')
@@ -64,10 +70,10 @@
  pName: '上海市',
  cCode: '3100',
  cName: '上海市',
  dCode: '310106',
  dName: '静安区'
  dCode: '310104',
  dName: '徐汇区'
})
areaStore.setSceneType({ label: '工地', value: '1' })
areaStore.setSceneType({ label: '全部场景', value: null })
// èŽ·å–æœ¬æœˆçš„æ‰€æœ‰å·¡æŸ¥ç»Ÿè®¡ä¿¡æ¯
subtaskStore.fetchTopTaskProgress(areaStore.area)
@@ -131,4 +137,10 @@
}
.problem-track {
}
.subtask-visual {
  margin-top: 50px;
  /* min-width: 450px;
  max-width: 600px; */
}
</style>
src/views/management/TaskStats.vue
@@ -10,7 +10,7 @@
      </el-row> -->
      <!-- <el-row> -->
      <TaskItem v-for="item in tasks" :key="item.guid" :value="item"></TaskItem>
      <TaskSummary></TaskSummary>
      <!-- <TaskSummary></TaskSummary> -->
      <!-- </el-row> -->
      <!-- <el-row>
@@ -74,11 +74,12 @@
    }
    const task = {
      name: tInfo.name,
      province: tInfo.provinceName,
      district: tInfo.districtName,
      totaltask: tInfo.totaltask,
      completetask: tInfo.completetask,
      // name: tInfo.name,
      // province: tInfo.provinceName,
      // district: tInfo.districtName,
      // totaltask: tInfo.totaltask,
      // completetask: tInfo.completetask,
      ...tInfo,
      _totaltask,
      _completetask,
      count: []
src/views/management/TaskSummary.vue
@@ -1,5 +1,6 @@
<template>
  <el-row> å·¡æŸ¥æ±‡æ€» </el-row>
  <BaseCard title="巡查汇总">
    <!-- <el-row> å·¡æŸ¥æ±‡æ€» </el-row> -->
  <el-segmented v-model="value" :options="options" block />
  <div v-show="value == '今日汇总'">
    <div ref="echart1" class="bar-chart"></div>
@@ -10,9 +11,10 @@
  <div v-show="value == '上周汇总'">
    <div ref="echart3" class="bar-chart"></div>
  </div>
  <div v-show="value == '月度汇总'">
    <div v-show="value == '本月汇总'">
    <div ref="echart4" class="bar-chart"></div>
  </div>
  </BaseCard>
</template>
<script setup>
@@ -30,8 +32,8 @@
const emits = defineEmits(['update:height'])
const value = ref('月度汇总')
const options = ['今日汇总', '本周汇总', '上周汇总', '月度汇总']
const value = ref('本月汇总')
const options = ['今日汇总', '本周汇总', '上周汇总', '本月汇总']
const subtaskToday = ref([])
const subtaskWeek = ref([])
@@ -139,7 +141,7 @@
    yAxis: [
      {
        type: 'category',
        data: ['整改数', '问题数', '完成量']
        data: ['复核数', '整改数', '问题数', '完成量']
      }
    ],
    series: series
@@ -153,6 +155,7 @@
  const totalCount = {
    numByTotal: {},
    numByDistrict: {},
    numByStreet: {},
    numByScene: {},
    numByUser: {}
  }
@@ -160,6 +163,7 @@
  const proCount = {
    numByTotal: {},
    numByDistrict: {},
    numByStreet: {},
    numByScene: {},
    numByUser: {}
  }
@@ -167,19 +171,24 @@
  const changeCount = {
    numByTotal: {},
    numByDistrict: {},
    numByStreet: {},
    numByScene: {},
    numByUser: {}
  }
  // // æŒ‰ç”¨æˆ·åˆ†ç±»
  // const userCount = {
  //   numByTotal: {},
  //   numByDistrict: {},
  //   numByScene: {},
  //   numByUser: {}
  // }
  // å¤æ ¸æ•°
  const reCheckCount = {
    numByTotal: {},
    numByDistrict: {},
    numByStreet: {},
    numByScene: {},
    numByUser: {}
  }
  const historySceneId = []
  dataList.forEach((d) => {
    const tName = '总计'
    const dName = d.subtask.districtname
    const townName = d.scene.townname
    const sType = d.sceneType
    const uName = d.subtask.deployerrealname
@@ -190,6 +199,10 @@
    // ä»»åŠ¡é‡åˆ†åŒºåŽ¿
    totalCount.numByDistrict[dName] = totalCount.numByDistrict[dName]
      ? totalCount.numByDistrict[dName] + 1
      : 1
    // ä»»åŠ¡é‡åˆ†è¡—é•‡
    totalCount.numByStreet[townName] = totalCount.numByStreet[townName]
      ? totalCount.numByStreet[townName] + 1
      : 1
    // ä»»åŠ¡é‡åˆ†åœºæ™¯ç±»åž‹
    totalCount.numByScene[sType] = totalCount.numByScene[sType]
@@ -205,6 +218,10 @@
    // é—®é¢˜æ•°åˆ†åŒºåŽ¿
    proCount.numByDistrict[dName] = proCount.numByDistrict[dName]
      ? proCount.numByDistrict[dName] + d.proCheckedNum
      : d.proCheckedNum
    // é—®é¢˜æ•°åˆ†è¡—镇
    proCount.numByStreet[townName] = proCount.numByStreet[townName]
      ? proCount.numByStreet[townName] + d.proCheckedNum
      : d.proCheckedNum
    // é—®é¢˜æ•°åˆ†åœºæ™¯ç±»åž‹
    proCount.numByScene[sType] = proCount.numByScene[sType]
@@ -223,6 +240,10 @@
    changeCount.numByDistrict[dName] = changeCount.numByDistrict[dName]
      ? changeCount.numByDistrict[dName] + d.changeCheckedNum
      : d.changeCheckedNum
    // æ•´æ”¹æ•°åˆ†è¡—镇
    changeCount.numByStreet[townName] = changeCount.numByStreet[townName]
      ? changeCount.numByStreet[townName] + d.changeCheckedNum
      : d.changeCheckedNum
    // æ•´æ”¹æ•°åˆ†åœºæ™¯ç±»åž‹
    changeCount.numByScene[sType] = changeCount.numByScene[sType]
      ? changeCount.numByScene[sType] + d.changeCheckedNum
@@ -231,12 +252,40 @@
    changeCount.numByUser[uName] = changeCount.numByUser[uName]
      ? changeCount.numByUser[uName] + d.changeCheckedNum
      : d.changeCheckedNum
    if (historySceneId.length == 0) {
      reCheckCount.numByTotal[tName] = 0
      reCheckCount.numByDistrict[dName] = 0
      reCheckCount.numByScene[sType] = 0
      reCheckCount.numByUser[uName] = 0
    } else if (historySceneId.indexOf(d.sceneId) != -1) {
      // å¤æ ¸æ•°æ€»è®¡
      reCheckCount.numByTotal[tName] = reCheckCount.numByTotal[tName]
        ? reCheckCount.numByTotal[tName] + 1
        : 1
      // å¤æ ¸æ•°åˆ†åŒºåŽ¿
      reCheckCount.numByDistrict[dName] = reCheckCount.numByDistrict[dName]
        ? reCheckCount.numByDistrict[dName] + 1
        : 1
      // å¤æ ¸æ•°åˆ†è¡—镇
      reCheckCount.numByStreet[townName] = reCheckCount.numByStreet[townName]
        ? reCheckCount.numByStreet[townName] + 1
        : 1
      // å¤æ ¸æ•°åˆ†åœºæ™¯ç±»åž‹
      reCheckCount.numByScene[sType] = reCheckCount.numByScene[sType]
        ? reCheckCount.numByScene[sType] + 1
        : 1
      // å¤æ ¸æ•°åˆ†ç”¨æˆ·
      reCheckCount.numByUser[uName] = reCheckCount.numByUser[uName]
        ? reCheckCount.numByUser[uName] + 1
        : 1
    } else {
      historySceneId.push(d.sceneId)
    }
  })
  let series = {}
  totalCount
  proCount
  changeCount
  parseSeries(series, reCheckCount)
  parseSeries(series, changeCount)
  parseSeries(series, proCount)
  parseSeries(series, totalCount)
@@ -291,12 +340,33 @@
        label: {
          show: true,
          formatter: '{c}'
          // position: [0, 10],
          // position: 'top',
        },
        data: [value]
      }
    }
  }
  // for (const key in c.numByStreet) {
  //   const value = c.numByStreet[key]
  //   if (series[key]) {
  //     series[key].data.push(value)
  //   } else {
  //     series[key] = {
  //       name: `街镇:${key}`,
  //       type: 'bar',
  //       stack: 'street',
  //       emphasis: {
  //         focus: 'series'
  //       },
  //       label: {
  //         show: true,
  //         formatter: '{c}'
  //         // position: 'top',
  //       },
  //       data: [value]
  //     }
  //   }
  // }
  for (const key in c.numByScene) {
    const value = c.numByScene[key]
    if (series[key]) {
src/views/visualization/SubtaskVisual.vue
@@ -1,149 +1,196 @@
<template>
  <el-scrollbar v-if="mapStore.focusMarker" :height="mapHeight">
    <el-card class="p-events-auto wrapper">
  <el-scrollbar v-if="mapStore.focusMarker" class="wrapper">
    <el-card class="p-events-auto">
      <el-row justify="space-between">
        <div class="font-small">{{ scene.name }}</div>
        <!-- <div class="font-small">{{ scene.name }}</div> -->
        <el-text size="large">{{ scene.name }}</el-text>
        <el-button icon="Close" circle @click="mapStore.focusMarker = undefined"></el-button>
      </el-row>
      <el-row justify="space-between">
        <el-text size="small">{{ '地址:' + scene.location }}</el-text>
      </el-row>
      <el-space class="m-t-8">
        <el-tag type="info" effect="plain">
          {{ scene.districtname + scene.townname }}
        </el-tag>
        <el-tag type="info" effect="plain">
          {{ scene.type }}
        </el-tag>
      </el-space>
      <el-divider></el-divider>
      <div class="font-small">状态:{{ subtask.status }}</div>
      <div class="font-small">计划:{{ $fm.formatYMD(subtask.planstarttime) }}</div>
      <div v-if="subtask.status != '未执行'" class="font-small">
        <span>执行:{{ $fm.formatYMDH(subtask.executionstarttime) }}</span>
        <span> - </span>
        <span>{{ $fm.formatYMDH(subtask.executionendtime) }}</span>
      </div>
      <div class="font-small">问题:</div>
      <el-row justify="space-between">
        <el-col :span="8" style="text-align: center">
          <el-text>状态:{{ subtask.status }}</el-text>
        </el-col>
        <el-col :span="8" style="text-align: center">
          <el-text>计划:{{ $fm.formatYMD(subtask.planstarttime) }}</el-text>
        </el-col>
        <el-col v-if="subtask.status != '未执行'" :span="8" style="text-align: center">
          <el-text>
            <span>执行:{{ $fm.formatH(subtask.executionstarttime) }}</span>
            <!-- <span> - </span>
          <span>{{ $fm.formatYMDH(subtask.executionendtime) }}</span> -->
          </el-text>
        </el-col>
      </el-row>
      <el-segmented v-model="value" :options="options" block />
      <div v-show="value == '现场问题'">
      <problem-item
        v-for="(item, i) in problemList"
        :key="item.guid"
        :index="i + 1"
        :problem="item"
      ></problem-item>
      <!-- <div v-for="item in problemList" :key="item.guid">
        {{ item.problemname }}
      </div> -->
      <!-- <el-timeline style="max-width: 600px">
        <el-timeline-item
          v-for="(activity, index) in activities"
          :key="index"
          :timestamp="activity.timestamp"
          :hide-timestamp="activity.running"
          :type="activity.running ? 'danger' : 'success'"
          :size="activity.running ? 'large' : 'normal'"
          :hollow="false"
        >
          {{ activity.content }}
        </el-timeline-item>
      </el-timeline> -->
        <el-empty v-if="problemList.length == 0" description="无现场问题">
          <template #image> </template>
        </el-empty>
      </div>
      <div v-show="value == '场景图片'">
        <FYImageSelectDialog
          readonly
          height="500px"
          :imageWidth="134"
          v-loading="scenePicLoading"
          :typeList="scenePicTypeList"
          :typeImgMap="scenePicTypeMap"
        ></FYImageSelectDialog>
      </div>
      <div v-show="value == '设备设施'">
      </div>
    </el-card>
  </el-scrollbar>
</template>
<script>
import { inject } from 'vue'
<script setup>
import { inject, ref, computed, watch } from 'vue'
import { useMapStore } from '@/stores/map.js'
import { mapStores } from 'pinia'
import { $fysp } from '@/api/index.js'
import problemApi from '@/api/fysp/problemApi.js'
import mediafileApi from '@/api/fysp/mediafileApi.js'
import deviceApi from '@/api/fysp/deviceApi'
/**
 * å…·ä½“巡查任务可视化
 * åŒ…括地图定位信息展示、巡查任务全流程平铺展示
 */
export default {
  setup() {
    const mapHeight = inject('mapHeight')
    const height = 'height:' + mapHeight
    return { height, mapHeight }
  },
  props: {
    // subtask: {
    //   type: Object,
    //   default: () => {
    //     return {
    //       guid: 'SMuheEkjswioSn7A',
    //       name: '中科生态数字港项目巡查中科生态数字港项目巡查',
    //       district: '金山区',
    //       planTime: '2024-06-04',
    //       startTime: '2024-06-04 13:31:26',
    //       endTime: '2024-06-04 13:33:37',
    //       userName: '朱正强',
    //       status: '已结束',
    //       total: 4,
    //       checked: 2
    //     }
    //   }
    // }
  },
  data() {
    return {
      // activities: [
      //   {
      //     content: '任务创建',
      //     timestamp: '2024-06-04 08:00',
      //     running: false
      //   },
      //   {
      //     content: '开始巡查',
      //     timestamp: '2024-06-04 09:00',
      //     running: false
      //   },
      //   {
      //     content: '结束巡查',
      //     timestamp: '2024-06-04 09:15',
      //     running: false
      //   },
      //   {
      //     content: '完成问题审核',
      //     timestamp: '2024-06-04 10:15',
      //     running: false
      //   },
      //   {
      //     content: '问题整改中...',
      //     timestamp: '2024-06-04 10:15',
      //     running: true
      //   }
      // ]
      problemList: []
    }
  },
  computed: {
    ...mapStores(useMapStore),
    subtask() {
      return this.mapStore.focusMarker ? this.mapStore.focusMarker.subtask : undefined
    },
    scene() {
      return this.mapStore.focusMarker ? this.mapStore.focusMarker.scene : undefined
    },
    inspection() {
      return this.mapStore.focusMarker ? this.mapStore.focusMarker.inspection : undefined
    }
  },
  watch: {
    subtask(nV, oV) {
const mapStore = useMapStore()
const subtask = computed(() => (mapStore.focusMarker ? mapStore.focusMarker.subtask : undefined))
const scene = computed(() => (mapStore.focusMarker ? mapStore.focusMarker.scene : undefined))
const inspection = computed(() =>
  mapStore.focusMarker ? mapStore.focusMarker.inspection : undefined
)
const value = ref('现场问题')
const options = ['现场问题', '场景图片', '设备设施']
const problemList = ref([])
const problemListLoading = ref(false)
const scenePicTypeList = ref([])
const scenePicTypeMap = ref(new Map())
const scenePicLoading = ref(false)
watch(subtask, (nV, oV) => {
      if (nV != undefined && nV != oV) {
        this.fetchProblem(nV.stguid)
    fetchProblem(nV.stguid)
    getRoutineByStGuid(nV.stguid)
  }
})
function fetchProblem(stguid) {
  problemListLoading.value = true
  problemApi
    .fetchProblems(stguid)
    .then((res) => {
      problemList.value = res
    })
    .finally(() => (problemListLoading.value = false))
}
// å›¾ç‰‡åˆ†ç±»
function getRoutineByStGuid(stguid) {
  scenePicLoading.value = true
  mediafileApi
    .getRoutineByStGuid(stguid)
    .then((res) => {
      let typeList = []
      let typeMap = new Map()
      const data = res.data
      for (const e of data) {
        let img = {
          url: $fysp.imgUrl + e.extension1 + e.guid + '.jpg',
          data: e
        }
        const businesstype = e.businesstype
        const businesstypeid = e.businesstypeid
        if (typeList.find((item) => item.typeId == businesstypeid) != undefined) {
          typeMap.get(businesstypeid).push(img)
        } else {
          typeList.push({
            typeId: businesstypeid,
            typeName: businesstype
          })
          typeMap.set(businesstypeid, [img])
      }
    }
  },
  methods: {
    fetchProblem(stguid) {
      problemApi.fetchProblems(stguid).then((res) => {
        this.problemList = res
      scenePicTypeList.value = typeList
      scenePicTypeMap.value = typeMap
    })
    .finally(() => (scenePicLoading.value = false))
}
function fetchDevices() {
  deviceApi.fetchDevices(scene.value.guid, this.currSelect.topDeviceTypeId).then((result) => {
    this.initList()
    if (result.data == null || result.data.length <= 0) {
      this.isEmpty = true
      return
    }
    // æ ‡å‡†åŒ–属性名
    for (let index = 0; index < result.data.length; index++) {
      var element = this.convertKeys(result.data[index])
      this.initFormData(element)
      // èŽ·å–è®¾å¤‡çŠ¶æ€ä¿¡æ¯
      let data = {
        deviceId: element.id,
        sceneId: element.sceneGuid,
        deviceTypeId: this.currSelect.topDeviceTypeId
      }
      deviceApi.fetchDeviceStatus(data).then((status) => {
        var statusData = status.data
        var imgPaths = []
        if (statusData) {
          if (statusData.length == 0) {
            this.formInfo.push(element)
            return
          }
          element = this.convertKeys(result.data[index])
          element = this.setDeviceType(element)
          element._picUrls = imgPaths
          for (let index = 0; index < statusData.length; index++) {
            const statusItem = statusData[index]
            // è®¾å¤‡å¯¹è±¡æ·»åŠ ä¸€ä¸ªå±žæ€§åˆ—è¡¨å±žæ€§ç”¨æ¥ä¿å­˜è®¾å¤‡çŠ¶æ€
            this.saveStatus(element, statusItem)
            element.dlLocation = statusItem.dlLocation
            this.formInfo.push(element)
          }
        }
      })
    }
  }
  })
}
</script>
<style scoped>
.wrapper {
  /* position: absolute; */
  top: 0;
  right: 0;
  /* background-color: wheat; */
  width: 450px;
  max-height: 800px;
}
.el-card {
src/views/visualization/SupervisionVisual.vue
@@ -49,12 +49,12 @@
        pName: '上海市',
        cCode: '3100',
        cName: '上海市',
        dCode: '310106',
        dName: '静安区'
        dCode: '310104',
        dName: '徐汇区'
      },
      sceneType: {
        label: '工地',
        value: '1'
        label: '全部场景',
        value: null
      },
      time: ''
    }