无人机管理后台前端(已迁走)
chenyao
2025-07-21 84d3ca7935f6cd913699cc5506cdc42a94fed640
Merge branch 'dev' of http://139.196.74.78:10010/r/drone/drone-web-manage into dev
18 files modified
4 files added
3203 ■■■■■ changed files
src/api/device/device.js 81 ●●●● patch | view | raw | blame | history
src/api/home/index.js 27 ●●●●● patch | view | raw | blame | history
src/assets/images/workbench/st1.svg 10 ●●●●● patch | view | raw | blame | history
src/permission.js 3 ●●●● patch | view | raw | blame | history
src/store/modules/user.js 2 ●●● patch | view | raw | blame | history
src/styles/element-ui.scss 23 ●●●●● patch | view | raw | blame | history
src/views/algorithmRepository/algorithmRepository.vue 10 ●●●● patch | view | raw | blame | history
src/views/dataCenter/components/searchData.vue 3 ●●●●● patch | view | raw | blame | history
src/views/device/airport.vue 776 ●●●● patch | view | raw | blame | history
src/views/job/components/TaskIntermediateContent/TaskIntermediateContent.vue 11 ●●●●● patch | view | raw | blame | history
src/views/job/components/TaskTop/TaskEvent.vue 7 ●●●● patch | view | raw | blame | history
src/views/job/components/TaskTop/TaskIndustry.vue 2 ●●● patch | view | raw | blame | history
src/views/tickets/orderLog.vue 7 ●●●●● patch | view | raw | blame | history
src/views/tickets/ticket.vue 157 ●●●●● patch | view | raw | blame | history
src/views/wel/components/backlog.vue 17 ●●●●● patch | view | raw | blame | history
src/views/wel/components/calendarBox.vue 107 ●●●●● patch | view | raw | blame | history
src/views/wel/components/flightStatistics.vue 446 ●●●●● patch | view | raw | blame | history
src/views/wel/components/flyratio.vue 148 ●●●● patch | view | raw | blame | history
src/views/wel/components/proportionStatic.vue 362 ●●●●● patch | view | raw | blame | history
src/views/wel/components/statistics.vue 32 ●●●● patch | view | raw | blame | history
src/views/wel/components/taskOutcome.vue 314 ●●●●● patch | view | raw | blame | history
src/views/wel/index.vue 658 ●●●●● patch | view | raw | blame | history
src/api/device/device.js
@@ -1,4 +1,4 @@
import request from '@/axios';
import request from '@/axios'
export const getList = (current, size, params) => {
  return request({
@@ -9,8 +9,8 @@
      current,
      size,
    },
  });
};
  })
}
export const remove = ids => {
  return request({
@@ -19,32 +19,32 @@
    params: {
      ids,
    },
  });
};
  })
}
export const add = row => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/add',
    method: 'post',
    data: row,
  });
};
  })
}
export const update = row => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/update',
    method: 'post',
    data: row,
  });
};
  })
}
export const operatePasswordUpdate = row => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/operatePasswordUpdate',
    method: 'post',
    data: row,
  });
};
  })
}
export const getDetail = id => {
  return request({
@@ -53,8 +53,8 @@
    params: {
      id,
    },
  });
};
  })
}
export const getOperatePassword = id => {
  return request({
@@ -63,24 +63,24 @@
    params: {
      id,
    },
  });
};
  })
}
export const getDeviceUpgradeInfo = (params) => {
  return request({
    url: '/drone-device-core/manage/api/v1/workspaces/firmware-release-notes/latest',
    method: 'get',
    params: params
  });
};
  })
}
export const ota = (workspace_id,row) => {
export const ota = (workspace_id, row) => {
  return request({
    url: `/drone-device-core/manage/api/v1/devices/${workspace_id}/devices/ota`,
    method: 'post',
    data: row,
  });
};
  })
}
export const getDeviceFirmwareList = (current, size, params) => {
  return request({
@@ -91,25 +91,25 @@
      current,
      size,
    },
  });
};
  })
}
export const upload = (workspace_id,formDatas) => {
export const upload = (workspace_id, formDatas) => {
  return request({
    url: `/drone-device-core/manage/api/v1/workspaces/${workspace_id}/firmwares/file/upload`,
    method: 'post',
    data: formDatas,
    headers: { "Content-Type": "multipart/form-data" },
  });
};
  })
}
export const updateFirmware = (row) => {
  return request({
    url: `/drone-device-core/manage/api/v1/workspaces/firmwares/update`,
    method: 'post',
    data: row,
  });
};
  })
}
export const deleteFirmware = ids => {
  return request({
@@ -118,29 +118,36 @@
    params: {
      ids
    }
  });
};
  })
}
export const getDevices = (workspace_id,device_sn) => {
export const getDevices = (workspace_id, device_sn) => {
  return request({
    url: `/drone-device-core/manage/api/v1/devices/${workspace_id}/devices/${device_sn}`,
    method: 'get',
  });
};
  })
}
export const getAirportList = () => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/getAirportList',
    method: 'get',
  });
};
  })
}
//可飞行机巢列表
export const getFlyingNestBy = (param) => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/getFlyingNestBy',
    method: 'post',
    data:param
  });
};
    data: param
  })
}
// 设备下线
export const deviceOffline = (dockSn) => {
  return request({
    url: `/drone-device-core/manage/api/v1/devices/deviceOffline?dockSn=${dockSn}`,
    method: 'get',
  })
}
src/api/home/index.js
@@ -56,6 +56,14 @@
    data,
  });
};
// 飞行统计111
export const tongjiNestApi = data => {
    return request({
        url: `/drone-device-core/manage/api/v1/devices/getAllDeviceStatistics`,
        method: 'post',
        data,
    })
}
// 飞行时长 里程 任务数
export const getFlyTime = data => {
  return request({
@@ -97,4 +105,21 @@
      areaCode
    },
  });
};
};
// 任务成果
export const getdisposeApi = data => {
    return request({
        url: `/drone-device-core/jobEvent/eventStatusLine`,
        method: 'post',
        data,
    })
}
// 任务成果
export const gettaskOutcomApi = data => {
    return request({
        url: `/blade-resource/attach/getManageAttachTypeStatistics`,
        method: 'post',
        data,
    })
}
src/assets/images/workbench/st1.svg
New file
@@ -0,0 +1,10 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_1421_9332)">
<path id="Vector" d="M14.3527 3.77465C14.2357 3.77465 14.1234 3.82113 14.0407 3.90387C13.958 3.98661 13.9115 4.09882 13.9115 4.21583V5.2774C13.23 4.11678 12.1947 3.20492 10.9573 2.67538C9.71995 2.14584 8.34552 2.02646 7.03539 2.33472C5.72525 2.64299 4.54825 3.36271 3.67683 4.38843C2.80541 5.41416 2.28535 6.69199 2.19285 8.03472C2.18815 8.09269 2.19495 8.15102 2.21287 8.20636C2.23079 8.26169 2.25947 8.31293 2.29727 8.35714C2.33507 8.40134 2.38124 8.43764 2.43312 8.46393C2.48499 8.49023 2.54156 8.50601 2.59956 8.51036C2.71083 8.5107 2.81812 8.46898 2.89993 8.39356C2.98175 8.31814 3.03204 8.21459 3.04073 8.10365C3.12938 6.92628 3.60019 5.81008 4.38155 4.92489C5.1629 4.0397 6.21203 3.43398 7.36929 3.19987C8.52656 2.96576 9.72863 3.11609 10.7926 3.62798C11.8566 4.13988 12.7242 4.98531 13.2635 6.03566H12.0917C11.9737 6.03566 11.8606 6.0825 11.7773 6.16589C11.6939 6.24927 11.647 6.36236 11.647 6.48028C11.647 6.5982 11.6939 6.71129 11.7773 6.79467C11.8606 6.87805 11.9737 6.9249 12.0917 6.9249H14.3527C14.4697 6.9249 14.5819 6.87842 14.6646 6.79568C14.7474 6.71295 14.7938 6.60073 14.7938 6.48373V4.21583C14.7938 4.09882 14.7474 3.98661 14.6646 3.90387C14.5819 3.82113 14.4697 3.77465 14.3527 3.77465ZM14.3527 8.50347C14.2947 8.49876 14.2364 8.50556 14.181 8.52348C14.1257 8.5414 14.0744 8.57009 14.0302 8.60789C13.986 8.64568 13.9497 8.69185 13.9234 8.74373C13.8972 8.79561 13.8814 8.85217 13.877 8.91017C13.7884 10.0875 13.3176 11.2037 12.5362 12.0889C11.7548 12.9741 10.7057 13.5799 9.54846 13.814C8.39119 14.0481 7.18912 13.8977 6.12515 13.3858C5.06118 12.8739 4.19354 12.0285 3.65424 10.9782H4.90193C5.01985 10.9782 5.13294 10.9313 5.21632 10.8479C5.2997 10.7646 5.34654 10.6515 5.34654 10.5335C5.34654 10.4156 5.2997 10.3025 5.21632 10.2192C5.13294 10.1358 5.01985 10.0889 4.90193 10.0889H2.64092C2.58298 10.0889 2.52562 10.1003 2.47209 10.1225C2.41856 10.1447 2.36993 10.1772 2.32896 10.2181C2.288 10.2591 2.2555 10.3077 2.23333 10.3613C2.21116 10.4148 2.19975 10.4722 2.19975 10.5301V12.7911C2.19975 12.909 2.24659 13.0221 2.32997 13.1055C2.41335 13.1889 2.52645 13.2357 2.64437 13.2357C2.76229 13.2357 2.87538 13.1889 2.95876 13.1055C3.04214 13.0221 3.08898 12.909 3.08898 12.7911V11.7226C3.77051 12.8833 4.80578 13.7951 6.04315 14.3247C7.28052 14.8542 8.65495 14.9736 9.96509 14.6653C11.2752 14.357 12.4522 13.6373 13.3236 12.6116C14.1951 11.5859 14.7151 10.3081 14.8076 8.96532C14.8104 8.90624 14.8013 8.84721 14.7808 8.79173C14.7603 8.73626 14.7288 8.68546 14.6883 8.64238C14.6478 8.59929 14.5991 8.56479 14.5449 8.54093C14.4908 8.51707 14.4325 8.50433 14.3733 8.50347H14.3527Z" fill="#1441FF"/>
</g>
<defs>
<clipPath id="clip0_1421_9332">
<rect width="17" height="17" fill="white"/>
</clipPath>
</defs>
</svg>
src/permission.js
@@ -47,9 +47,8 @@
  store.commit('SET_IS_MENU', isMenu === undefined)
  const menuAll = getStore({ name: 'menuAll' })
  if (getToken()) {
    if (!menuAll) {
    if (!menuAll?.length) {
      store.dispatch('GetMenu').then(data => {
        if (data.length !== 0) {
          router.$avueRouter.formatRoutes(data, true)
src/store/modules/user.js
@@ -273,7 +273,7 @@
        getRoutes(tenantId).then(res => {
          const data = res.data.data;
          let menu = deepClone(data);
          menu.forEach(ele => formatPath(ele, true));
          menu?.forEach(ele => formatPath(ele, true));
          commit('SET_MENU', menu);
          commit('SET_MENU_ALL', menu);
          dispatch('GetButtons');
src/styles/element-ui.scss
@@ -115,4 +115,27 @@
    display: flex;
    flex-direction: column;
  }
}
.ztzf-public-general-avue-crud {
  .el-table {
    tr {
      min-height: 30px !important;
      line-height: 30px !important;
      box-sizing: border-box !important;
    }
    tr th,
    tr td {
      padding: 6px 0 !important;
      min-height: 30px !important;
      line-height: 30px !important;
      box-sizing: border-box !important;
      div.cell {
        min-height: 30px !important;
        line-height: 30px !important;
      }
    }
  }
}
src/views/algorithmRepository/algorithmRepository.vue
@@ -181,8 +181,8 @@
};
// 日期选择
const changeselect = () => {
  params.value.start_date = taskData.value.length ? `${taskData.value[0]} 00:00:00` : null;
  params.value.end_date = taskData.value.length ? `${taskData.value[1]} 23:59:59` : null;
  params.value.start_date = taskData.value?.length ? `${taskData.value[0]} 00:00:00` : null;
  params.value.end_date = taskData.value?.length ? `${taskData.value[1]} 23:59:59` : null;
};
// 处理日期清空
const handleDateClear = () => {
@@ -192,6 +192,12 @@
  getList();
};
const handleSearch = () => {
// console.log('taskData',taskData.value);
if(taskData.value === null){
    params.value.start_date = null; // 重置开始日期参数
  params.value.end_date = null; // 重置结束日期参数
}
  getList();
  
};
src/views/dataCenter/components/searchData.vue
@@ -68,6 +68,7 @@
              :key="item.value"
              :label="item.label"
              :value="item.value"
         :disabled="searchForm.photoType !== '' && item.value === '4'"
            />
          </el-select>
        </el-form-item>
@@ -163,10 +164,12 @@
  {
    value: '5',
    label: '全景',
  },
  {
    value: '4',
    label: '正射',
  },
];
const CategoryOption = [
src/views/device/airport.vue
@@ -1,34 +1,12 @@
<template>
  <basic-main-content>
    <avue-crud
      :option="option"
      :table-loading="loading"
      :data="data"
      v-model:page="page"
      ref="crud"
      @row-del="rowDel"
      v-model="form"
      :permission="permissionList"
      @row-update="rowUpdate"
      @row-save="rowSave"
      :before-open="beforeOpen"
      @search-change="searchChange"
      @search-reset="searchReset"
      @selection-change="selectionChange"
      @current-change="currentChange"
      @size-change="sizeChange"
      @refresh-change="refreshChange"
      @on-load="onLoad"
      @tree-load="treeLoad"
    >
    <avue-crud :option="option" :table-loading="loading" :data="data" v-model:page="page" ref="crud" @row-del="rowDel"
      v-model="form" :permission="permissionList" @row-update="rowUpdate" @row-save="rowSave" :before-open="beforeOpen"
      @search-change="searchChange" @search-reset="searchReset" @selection-change="selectionChange"
      @current-change="currentChange" @size-change="sizeChange" @refresh-change="refreshChange" @on-load="onLoad"
      @tree-load="treeLoad">
      <template #menu-left>
        <el-button
          type="danger"
          icon="el-icon-delete"
          plain
          v-if="permission.device_delete"
          @click="handleDelete"
          >删 除
        <el-button type="danger" icon="el-icon-delete" plain v-if="permission.device_delete" @click="handleDelete">删 除
        </el-button>
      </template>
      <template #dept_name="{ row }">
@@ -47,69 +25,46 @@
      <template #firmware_status="{ row }">
        <div v-if="row.firmware_status == 1" class="onlineStatus1">
          <el-tag type="info">无需升级</el-tag>
          <el-tag type="primary" effect="dark" class="active-element" @click="rollFirmware(row)"
            >版本管理</el-tag
          >
          <el-tag type="primary" effect="dark" class="active-element" @click="rollFirmware(row)">版本管理</el-tag>
          <!-- <div style="background-color: #f1f1f1; border-radius: 4px">无需升级</div> -->
        </div>
        <div v-if="row.firmware_status == 2" class="onlineStatus1">
          <!-- <el-tag type="success" effect="dark" class="active-element" @click="updateFirmware(row)"
            >升级固件</el-tag
          > -->
          <el-tag type="primary" effect="dark" class="active-element" @click="rollFirmware(row)"
            >版本管理</el-tag
          >
          <el-tag type="primary" effect="dark" class="active-element" @click="rollFirmware(row)">版本管理</el-tag>
          <!-- <div @click="updateFirmware(row)" class="firmware_status">升级固件</div> -->
        </div>
        <div v-if="row.firmware_status == 3" class="onlineStatus">
          <el-tag type="success" effect="dark" class="active-element" @click="updateFirmware(row)"
            >一致性升级</el-tag
          >
          <el-tag type="success" effect="dark" class="active-element" @click="updateFirmware(row)">一致性升级</el-tag>
          <!-- <div @click="updateFirmware(row)" class="firmware_status">一致性升级</div> -->
        </div>
        <div v-if="row.firmware_status == 4" class="onlineStatus">
          <el-tag effect="dark" class="active-element" @click="updateFirmware(row)">升级中</el-tag>
          <el-progress
            v-if="row.firmware_progress > 0 && row.firmware_progress < 100"
            :percentage="row.firmware_progress"
          ></el-progress>
          <el-progress v-if="row.firmware_progress > 0 && row.firmware_progress < 100"
            :percentage="row.firmware_progress"></el-progress>
        </div>
      </template>
      <template #mode_code="{ row }">
        <span
          class="text"
          v-if="row.domain == 3"
          :style="row.mode_code != '-1' ? 'color: #00ee8b' : 'color: #de5e5e'"
        >
        <span class="text" v-if="row.domain == 3" :style="row.mode_code != '-1' ? 'color: #00ee8b' : 'color: #de5e5e'">
          {{ getDockModeText(row.mode_code) }}
        </span>
      </template>
      <template #menu="scope">
        <el-button
          type="primary"
          text
          icon="el-icon-paperclip"
          v-if="permission.oss_set"
          @click.stop="handleOpenOssSet(scope.row, scope.index)"
          >存储配置
        <el-button type="primary" text icon="el-icon-paperclip" v-if="permission.oss_set"
          @click.stop="handleOpenOssSet(scope.row, scope.index)">存储配置
        </el-button>
        <el-button
          type="primary"
          text
          icon="el-icon-share"
          v-if="permission.per_share && scope.row.domain == 3"
          @click.stop="handleOpenDevicePerShare(scope.row, scope.index)"
          >机场授权
        <el-button type="primary" text icon="el-icon-share" v-if="permission.per_share && scope.row.domain == 3"
          @click.stop="handleOpenDevicePerShare(scope.row, scope.index)">机场授权
        </el-button>
        <el-button
          type="primary"
          text
          icon="el-icon-position"
          :disabled="!scope.row.status"
        <el-button type="primary" text icon="el-icon-position" :disabled="!scope.row.status"
          v-if="permission.rang_con && scope.row.domain == 3"
          @click.stop="handleOpenRemoteDebugging(scope.row, scope.index)"
          >远程调试
          @click.stop="handleOpenRemoteDebugging(scope.row, scope.index)">远程调试
        </el-button>
        <el-button type="primary" text icon="el-icon-setting" v-if="permission.fly_device_offline"
          @click.stop="handleDeviceOffline(scope.row)">设备下线
        </el-button>
      </template>
      <!-- 添加行政区划显示模板 -->
@@ -128,22 +83,11 @@
      </template>
    </el-dialog>
    <el-dialog
      title="存储配置"
      append-to-body
      v-model="ossSetBox"
      @close="handleOssSetClose"
      width="600px"
    >
    <el-dialog title="存储配置" append-to-body v-model="ossSetBox" @close="handleOssSetClose" width="600px">
      <div class="onlineStatus">
        存储对象配置:
        <el-select v-model="ossEnd" clearable placeholder="请选择存储对象" style="width: 450px">
          <el-option
            v-for="item in ossList"
            :key="item.id"
            :label="item.name + ': ' + item.endpoint"
            :value="item.id"
          >
          <el-option v-for="item in ossList" :key="item.id" :label="item.name + ': ' + item.endpoint" :value="item.id">
            <span style="float: left">{{ item.name + ': ' + item.endpoint }}</span>
            <span v-if="item.status == 1" class="status1">{{ item.categoryName }}</span>
            <span v-if="item.status == 2" class="status2">{{ item.categoryName }}</span>
@@ -162,18 +106,9 @@
    <el-dialog title="固件版本管理" append-to-body v-model="rollFirmwareBox" width="455px">
      <div class="onlineStatus">
        固件版本升级/回退:
        <el-select
          v-model="rollFirmVersion"
          clearable
          placeholder="请选择固件版本"
          style="width: 240px"
        >
          <el-option
            v-for="item in firmList"
            :key="item.firmware_version"
            :label="item.firmware_version"
            :value="item.firmware_version"
          />
        <el-select v-model="rollFirmVersion" clearable placeholder="请选择固件版本" style="width: 240px">
          <el-option v-for="item in firmList" :key="item.firmware_version" :label="item.firmware_version"
            :value="item.firmware_version" />
        </el-select>
      </div>
      <template #footer>
@@ -184,29 +119,21 @@
      </template>
    </el-dialog>
    <el-drawer
      class="ztzf-drawer-body-basic-container"
      title="机场授权管理"
      size="60%"
      append-to-body
      v-model="devicePerShareVisible"
      :direction="'rtl'"
    >
    <el-drawer class="ztzf-drawer-body-basic-container" title="机场授权管理" size="60%" append-to-body
      v-model="devicePerShareVisible" :direction="'rtl'">
      <DevicePerShare ref="devicePerShare" />
    </el-drawer>
    <el-drawer :title="operateTitle" append-to-body v-model="remoteDebuggingShow" size="40%">
      <DockControlPanel
        v-if="remoteDebuggingShow"
        :sn="curDeviceInfo.device_sn"
        :deviceInfo="curDeviceInfo"
      >
      <DockControlPanel v-if="remoteDebuggingShow" :sn="curDeviceInfo.device_sn" :deviceInfo="curDeviceInfo">
      </DockControlPanel>
    </el-drawer>
  </basic-main-content>
</template>
<script>
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  getList,
  remove,
@@ -217,24 +144,25 @@
  ota,
  getDeviceFirmwareList,
  getDevices,
} from '@/api/device/device';
import { deleteByOssId, addOrUpdate as addOrUpdateOssBind } from '@/api/device/ossBind';
import { getListPage as getOssList } from '@/api/resource/oss';
import { getLazyTree } from '@/api/base/region';
import { EDockModeText, EDockModeCode } from '@/types/device';
import { mapGetters } from 'vuex';
import FirmwareManage from './components/firmwareManage.vue';
import DevicePerShare from './components/devicePerShare.vue';
import DockControlPanel from './components/DockControlPanel.vue';
import { getWebsocketUrl } from '@/websocket/util/config';
import ConnectWebSocket from '@/websocket';
  deviceOffline
} from '@/api/device/device'
import { deleteByOssId, addOrUpdate as addOrUpdateOssBind } from '@/api/device/ossBind'
import { getListPage as getOssList } from '@/api/resource/oss'
import { getLazyTree } from '@/api/base/region'
import { EDockModeText, EDockModeCode } from '@/types/device'
import { mapGetters } from 'vuex'
import FirmwareManage from './components/firmwareManage.vue'
import DevicePerShare from './components/devicePerShare.vue'
import DockControlPanel from './components/DockControlPanel.vue'
import { getWebsocketUrl } from '@/websocket/util/config'
import ConnectWebSocket from '@/websocket'
export default {
  components: {
    FirmwareManage,
    DevicePerShare,
    DockControlPanel,
  },
  data() {
  data () {
    return {
      bindOssId: null,
      deviceSn: '',
@@ -462,22 +390,10 @@
                trigger: 'change',
              },
            ],
            change: ({ value }) => {
              if (!value) {
                this.form.area_code = null;
              } else {
                if (typeof value === 'string' && value.includes(',')) {
                  const codes = value.split(',');
                  this.form.area_code = codes[codes.length - 1];
                } else {
                  this.form.area_code = value;
                }
              }
            },
            lazy: true,
            lazyLoad(node, resolve) {
              let level = node.level;
              let list = [];
            lazyLoad (node, resolve) {
              let level = node.level
              let list = []
              let callback = () => {
                resolve(
                  (list || []).map(ele => ({
@@ -485,26 +401,26 @@
                    value: ele.value,
                    leaf: level >= 2,
                  }))
                );
              };
                )
              }
              if (level === 0) {
                getLazyTree('000000000000').then(res => {
                  list = res.data.data;
                  callback();
                });
                  list = res.data.data
                  callback()
                })
              } else if (level === 1) {
                getLazyTree(node.value).then(res => {
                  list = res.data.data;
                  callback();
                });
                  list = res.data.data
                  callback()
                })
              } else if (level === 2) {
                getLazyTree(node.value).then(res => {
                  list = res.data.data;
                  callback();
                });
                  list = res.data.data
                  callback()
                })
              } else {
                callback();
                callback()
              }
            },
          },
@@ -547,7 +463,7 @@
              },
            ],
          },
           {
          {
            label: '在线状态',
            prop: 'cnstatus',
            hide: true,
@@ -561,7 +477,7 @@
            prop: 'status',
            addDisplay: false,
            editDisplay: false,
             viewDisplay: false,
            viewDisplay: false,
            labelWidth: 130,
            slot: true,
            width: 100,
@@ -617,241 +533,263 @@
        ConsistencyUpgrade: 3, // 一致性升级
        DuringUpgrade: 4, // 升级中
      },
    };
    }
  },
  computed: {
    ...mapGetters(['permission']),
    permissionList() {
    permissionList () {
      return {
        addBtn: this.validData(this.permission.air_add, true),
        viewBtn: this.validData(this.permission.air_view, true),
        delBtn: this.validData(this.permission.air_delete, true),
        editBtn: this.validData(this.permission.air_edit, true),
      };
      }
    },
    ids() {
      let ids = [];
    ids () {
      let ids = []
      this.selectionList.forEach(ele => {
        ids.push(ele.id);
      });
      return ids.join(',');
        ids.push(ele.id)
      })
      return ids.join(',')
    },
  },
  destroyed() {
  destroyed () {
    this.websocketMap.forEach((k, v) => {
      if (null != v) {
        v.close();
        this.websocketMap.delete(k);
        this.webSocketIdSet.delete(k);
        v.close()
        this.websocketMap.delete(k)
        this.webSocketIdSet.delete(k)
      }
    });
    })
  },
  methods: {
    getDockModeText(value) {
      return EDockModeText[value] || '';
    getDockModeText (value) {
      return EDockModeText[value] || ''
    },
    // 关闭所有的webscoket
    closeAllWebsoket() {
    closeAllWebsoket () {
      this.websocketMap.forEach((k, v) => {
        if (null != v) {
          v.close();
          this.websocketMap.delete(k);
          this.webSocketIdSet.delete(k);
          v.close()
          this.websocketMap.delete(k)
          this.webSocketIdSet.delete(k)
        }
      });
      })
    },
    // 连接webSocket
    connectWebSocket(data) {
      const webSocketId = data.workspace_id;
    connectWebSocket (data) {
      const webSocketId = data.workspace_id
      if (!this.webSocketIdSet.has(webSocketId)) {
        // 防止重复连接
        this.webSocketIdSet.add(webSocketId);
        const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + data.workspace_id;
        this.webSocketIdSet.add(webSocketId)
        const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + data.workspace_id
        // 监听ws 消息
        this.useConnectWebSocket(webSocketId, webSorketUrl);
        this.useConnectWebSocket(webSocketId, webSorketUrl)
      }
    },
    useConnectWebSocket(webSocketId, url) {
      const websocket = new ConnectWebSocket(url);
    useConnectWebSocket (webSocketId, url) {
      const websocket = new ConnectWebSocket(url)
      // 加入 webscoket map
      this.websocketMap.set(webSocketId, websocket);
      websocket?.registerMessageHandler(this.messageHandler);
      websocket?.initSocket();
      this.websocketMap.set(webSocketId, websocket)
      websocket?.registerMessageHandler(this.messageHandler)
      websocket?.initSocket()
    },
    async messageHandler(payload) {
    async messageHandler (payload) {
      if (!payload) {
        return;
        return
      }
      if (payload.biz_code != 'ota_progress') {
        return;
        return
      }
      var data = payload.data;
      var data = payload.data
      this.data.forEach(e => {
        if (e.device_sn == data.sn && data.output.status == 'in_progress') {
          e.firmware_progress = data.output.progress.percent;
          e.firmware_progress = data.output.progress.percent
        }
        if (e.device_sn == data.sn && data.output.status == 'ok') {
          // 升级完成修改状态
          e.firmware_status = 1;
          const webscoket = this.websocketMap.get(e.workspace_id);
          e.firmware_status = 1
          const webscoket = this.websocketMap.get(e.workspace_id)
          // 关闭
          webscoket?.close();
          webscoket?.close()
          //删除对应的webscoket
          this.websocketMap.delete(e.workspace_id);
          this.webSocketIdSet.delete(e.workspace_id);
          this.websocketMap.delete(e.workspace_id)
          this.webSocketIdSet.delete(e.workspace_id)
        }
      });
      })
    },
    init() {
    init () {
      (this.page = {
        pageSize: 10,
        currentPage: 1,
        total: 0,
      }),
        this.onLoad(this.page);
        this.onLoad(this.page)
    },
    // 打开权限分享页面
    handleOpenDevicePerShare(row) {
      var that = this;
      this.devicePerShareVisible = true;
    handleOpenDevicePerShare (row) {
      var that = this
      this.devicePerShareVisible = true
      this.$nextTick(() => {
        that.$refs.devicePerShare.init(row);
      });
        that.$refs.devicePerShare.init(row)
      })
    },
    // 打开远程调试
    handleOpenRemoteDebugging(row) {
      this.curDeviceInfo = row;
      this.remoteDebuggingShow = true;
      this.operateTitle = row.nickname + ' - ' + row.device_sn;
    handleOpenRemoteDebugging (row) {
      this.curDeviceInfo = row
      this.remoteDebuggingShow = true
      this.operateTitle = row.nickname + ' - ' + row.device_sn
    },
    // 设备下线
    handleDeviceOffline (row) {
      ElMessageBox.confirm('确定下线该设备吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => {
          deviceOffline(row.device_sn)
            .then(res => {
              ElMessage.success('下线成功')
              this.init()
            })
            .catch(error => {
              ElMessage.error('下线失败')
            })
        })
        .catch(() => { })
    },
    // 打开存储对象配置页面
    handleOpenOssSet(row) {
      var that = this;
      this.deviceSn = row.device_sn;
      this.ossSetBox = true;
      this.bindOssId = row.oss_id;
      this.ossEnd = row.oss_id;
    handleOpenOssSet (row) {
      var that = this
      this.deviceSn = row.device_sn
      this.ossSetBox = true
      this.bindOssId = row.oss_id
      this.ossEnd = row.oss_id
      // 查询下拉列表
      const param = {
        categoryKeys: '1,3,6',
      };
      }
      getOssList(1, 50, param).then(res => {
        const data = res.data.data;
        that.ossList = data.records;
      });
        const data = res.data.data
        that.ossList = data.records
      })
    },
    // 打开固件管理页面
    handleOpenFirmwarm(row) {
      var that = this;
      this.firmwareManageVisible = true;
    handleOpenFirmwarm (row) {
      var that = this
      this.firmwareManageVisible = true
      this.$nextTick(() => {
        that.$refs.firmwareManage.init(row);
      });
        that.$refs.firmwareManage.init(row)
      })
    },
    // 版本回退点击事件
    rollFirmware(row) {
    rollFirmware (row) {
      if (row.status == 0) {
        this.$message({
          type: 'warning',
          message: '设备不在线!',
        });
        return;
        })
        return
      }
      // 如果是机场,查询飞机对应的信息
      if (row.domain === 3) {
        // 查询子设备名称
        getDevices(row.workspace_id, row.child_sn).then(res => {
          const data = res.data.data;
          that.firmwareInfo['child_device_name'] = data.device_name;
          that.firmwareInfo['child_version'] = data.firmware_version;
          that.firmwareInfo['child_status'] = data.status;
        });
          const data = res.data.data
          that.firmwareInfo['child_device_name'] = data.device_name
          that.firmwareInfo['child_version'] = data.firmware_version
          that.firmwareInfo['child_status'] = data.status
        })
      }
      var that = this;
      this.rollFirmwareBox = true;
      var that = this
      this.rollFirmwareBox = true
      const param = {
        device_name: row.device_name,
      };
      }
      // 获取固件最新版本信息
      getDeviceFirmwareList(1, 20, param).then(res => {
        that.firmList = [];
        const data = res.data.data.records;
        that.firmList = []
        const data = res.data.data.records
        if (data.length === 0) {
          return;
          return
        }
        data.forEach(e => {
          if (e.firmware_version != row.firmware_version) {
            that.firmList.push(e);
            that.firmList.push(e)
          }
        });
        that.firmwareInfo['device_name'] = row.device_name;
        that.firmwareInfo['sn'] = row.device_sn;
        })
        that.firmwareInfo['device_name'] = row.device_name
        that.firmwareInfo['sn'] = row.device_sn
        // 3-普通升级,2-一致性升级;
        that.firmwareInfo['firmware_upgrade_type'] = 3;
        that.firmwareInfo['workspaceId'] = row.workspace_id;
        that.firmwareInfo['firmware_version'] = row.firmware_version;
        that.firmwareInfo['status'] = row.status;
        that.firmwareInfo['firmware_upgrade_type'] = 3
        that.firmwareInfo['workspaceId'] = row.workspace_id
        that.firmwareInfo['firmware_version'] = row.firmware_version
        that.firmwareInfo['status'] = row.status
        // domain 0:飞机  3:机场
        that.firmwareInfo['domain'] = row.domain;
        that.firmwareInfo['child_sn'] = row.child_sn;
        that.firmwareInfo['now_version'] = row.firmware_version;
      });
        that.firmwareInfo['domain'] = row.domain
        that.firmwareInfo['child_sn'] = row.child_sn
        that.firmwareInfo['now_version'] = row.firmware_version
      })
    },
    // 比较版本大小
    compare(version1, version2) {
      let arr1 = version1.split('.');
      let arr2 = version2.split('.');
      let length = Math.max(arr1.length, arr2.length);
    compare (version1, version2) {
      let arr1 = version1.split('.')
      let arr2 = version2.split('.')
      let length = Math.max(arr1.length, arr2.length)
      for (let i = 0; i < length; i++) {
        const n1 = Number(arr1[i] || 0);
        const n2 = Number(arr2[i] || 0);
        const n1 = Number(arr1[i] || 0)
        const n2 = Number(arr2[i] || 0)
        // version1 > version2 返回1,如果 version1 < version2 返回-1,不然返回0
        if (n1 > n2) return 1;
        if (n1 < n2) return -1;
        if (n1 > n2) return 1
        if (n1 < n2) return -1
      }
      return 0;
      return 0
    },
    // 版本回退
    rollFirmwareConfirm() {
    rollFirmwareConfirm () {
      // 无人机升级/回滚
      if (this.rollFirmVersion == '') {
        this.$message({
          type: 'warning',
          message: '请先选择回退的固件版本!',
        });
        return;
        })
        return
      }
      var that = this;
      var arr = [];
      that.firmwareInfo['product_version'] = this.rollFirmVersion;
      var that = this
      var arr = []
      that.firmwareInfo['product_version'] = this.rollFirmVersion
      // 机场信息设置
      arr.push(that.firmwareInfo);
      arr.push(that.firmwareInfo)
      // 判断是机场升级还是飞机升级,如果是机场升级则把无人机信息一起带过去
      if (that.firmwareInfo.domain === 3) {
        // 判断当前机场和无人机固件版本是否一致
        var dockFlag = this.compare(
          this.firmwareInfo.firmware_version,
          this.firmwareInfo.child_version
        );
        )
        // 机场和无人机版本一致情况
        if (dockFlag == 0) {
          if (!this.firmwareInfo.status || !this.firmwareInfo.child_status) {
            this.$message({
              type: 'warning',
              message: '请先更新无人机固件版本后再进行机场固件版本更新!',
            });
            return;
            })
            return
          }
          // 只有同时在线并且是升级才能操作
          // 判断升级还是降级,不能降级
          var upDownFlag = this.compare(this.rollFirmVersion, this.firmwareInfo.firmware_version);
          var upDownFlag = this.compare(this.rollFirmVersion, this.firmwareInfo.firmware_version)
          if (upDownFlag == -1) {
            this.$message({
              type: 'warning',
              message: '请先回滚无人机固件版本后再进行机场固件版本更新!',
            });
            return;
            })
            return
          }
          // 同时升级操作
          // 无人机信息设置
@@ -860,229 +798,229 @@
            sn: that.firmwareInfo.child_sn,
            product_version: this.rollFirmVersion,
            firmware_upgrade_type: 3,
          });
          })
        } else {
          // 版本不一致情况
          // 比对需要改变的版本号和无人机版本号是否一致,不一致提示不能操作
          var verFlag = this.compare(this.rollFirmVersion, this.firmwareInfo.child_version);
          var verFlag = this.compare(this.rollFirmVersion, this.firmwareInfo.child_version)
          if (verFlag == -1 || verFlag == 1) {
            this.$message({
              type: 'warning',
              message: '更新版本与当前无人机固件版本不一致,请更新和无人机一致的版本!',
            });
            return;
            })
            return
          }
          // 需要改变的版本号和无人机版本号如果是一致,进行单独的机场升级/回滚操作
        }
      }
      ota(that.firmwareInfo.workspaceId, arr).then(res => {
        that.firmwareVersion = '';
        that.firmwareInfo = {};
        that.rollFirmwareBox = false;
        that.firmwareVersion = ''
        that.firmwareInfo = {}
        that.rollFirmwareBox = false
        this.$message({
          type: 'success',
          message: '操作成功!',
        });
        this.onLoad(this.page);
        done();
      });
        })
        this.onLoad(this.page)
        done()
      })
    },
    // 升级固件按钮事件
    updateFirmware(row) {
    updateFirmware (row) {
      if (row.status == 0) {
        this.$message({
          type: 'warning',
          message: '设备不在线!',
        });
        return;
        })
        return
      }
      var that = this;
      this.firmwareBox = true;
      var that = this
      this.firmwareBox = true
      const param = {
        device_name: row.device_name,
      };
      }
      // 获取固件最新版本信息
      getDeviceUpgradeInfo(param).then(res => {
        const data = res.data.data;
        const data = res.data.data
        if (data.length === 0) {
          return;
          return
        }
        that.firmwareVersion = data[0].product_version;
        that.firmwareInfo['device_name'] = row.device_name;
        that.firmwareInfo['sn'] = row.device_sn;
        that.firmwareInfo['product_version'] = data[0].product_version;
        that.firmwareVersion = data[0].product_version
        that.firmwareInfo['device_name'] = row.device_name
        that.firmwareInfo['sn'] = row.device_sn
        that.firmwareInfo['product_version'] = data[0].product_version
        // 3-普通升级,2-一致性升级;
        that.firmwareInfo['firmware_upgrade_type'] =
          row.firmware_status === that.DeviceFirmwareStatusEnum.ToUpgraded
            ? that.DeviceFirmwareTypeEnum.ToUpgraded
            : that.DeviceFirmwareTypeEnum.ConsistencyUpgrade;
        that.firmwareInfo['workspaceId'] = row.workspace_id;
      });
            : that.DeviceFirmwareTypeEnum.ConsistencyUpgrade
        that.firmwareInfo['workspaceId'] = row.workspace_id
      })
    },
    // 确定升级固件版本
    updateFirmwareConfirm() {
      var that = this;
      var arr = [];
      arr.push(that.firmwareInfo);
    updateFirmwareConfirm () {
      var that = this
      var arr = []
      arr.push(that.firmwareInfo)
      ota(that.firmwareInfo.workspaceId, arr).then(res => {
        that.firmwareVersion = '';
        that.firmwareInfo = {};
        that.firmwareBox = false;
        that.firmwareVersion = ''
        that.firmwareInfo = {}
        that.firmwareBox = false
        this.$message({
          type: 'success',
          message: '操作成功!',
        });
        this.onLoad(this.page);
        done();
      });
        })
        this.onLoad(this.page)
        done()
      })
    },
    handleOssSetClose() {
      this.ossEnd = '';
      this.deviceSn = '';
      this.bindOssId = null;
    handleOssSetClose () {
      this.ossEnd = ''
      this.deviceSn = ''
      this.bindOssId = null
    },
    // 存储对象配置确定
    ossSetConfirm() {
      var that = this;
    ossSetConfirm () {
      var that = this
      // 判断是否已选择
      if (!this.ossEnd) {
        this.$message({
          type: 'warning',
          message: '请选择对于的存储对象!',
        });
        return;
        })
        return
      }
      // 新增或者修改操作
      const data = {
        oss_id: this.ossEnd,
        sn: this.deviceSn,
      };
      }
      // 新增
      addOrUpdateOssBind(data).then(res => {
        this.$message({
          type: 'success',
          message: '操作成功!',
        });
        that.ossSetBox = false;
        this.onLoad(this.page);
        done();
      });
        })
        that.ossSetBox = false
        this.onLoad(this.page)
        done()
      })
    },
    // 取消配置
    cancelOssSetConfirm() {
      var that = this;
    cancelOssSetConfirm () {
      var that = this
      // 判断是否已有配置
      if (!this.bindOssId) {
        this.$message({
          type: 'warning',
          message: '当前机场尚未配置存储对象信息!',
        });
        return;
        })
        return
      }
      deleteByOssId(this.bindOssId).then(() => {
        that.ossSetBox = false;
        this.onLoad(this.page);
        that.ossSetBox = false
        this.onLoad(this.page)
        this.$message({
          type: 'success',
          message: '操作成功!',
        });
      });
        })
      })
    },
    // 取消按钮 oss
    cancelOssSet() {
      this.ossEnd = '';
      this.deviceSn = '';
      this.bindOssId = null;
      this.ossSetBox = false;
    cancelOssSet () {
      this.ossEnd = ''
      this.deviceSn = ''
      this.bindOssId = null
      this.ossSetBox = false
    },
    rowSave(row, done, loading) {
      let areaCode = row.area_code;
    rowSave (row, done, loading) {
      let areaCode = row.area_code
      if (Array.isArray(areaCode)) {
        areaCode = areaCode[areaCode.length - 1];
        areaCode = areaCode[areaCode.length - 1]
      } else if (typeof areaCode === 'string' && areaCode.includes(',')) {
        const codes = areaCode.split(',');
        areaCode = codes[codes.length - 1];
        const codes = areaCode.split(',')
        areaCode = codes[codes.length - 1]
      }
      row.area_code = areaCode || null;
      row.area_code = areaCode || null
      add(row).then(
        () => {
          this.onLoad(this.page);
          this.onLoad(this.page)
          this.$message({
            type: 'success',
            message: '操作成功!',
          });
          done();
          })
          done()
        },
        error => {
          window.console.log(error);
          loading();
          window.console.log(error)
          loading()
        }
      );
      )
    },
    rowUpdate(row, index, done, loading) {
    rowUpdate (row, index, done, loading) {
      const submitData = {
        ...row,
        area_code: row.area_code.split(',').pop(),
        insure_start_time: row.duration_of_insurance[0],
        insure_expired_time: row.duration_of_insurance[1],
      };
      }
      delete submitData.duration_of_insurance;
      delete submitData.duration_of_insurance
      update(submitData).then(
        () => {
          this.onLoad(this.page);
          this.onLoad(this.page)
          this.$message({
            type: 'success',
            message: '操作成功!',
          });
          done();
          })
          done()
        },
        error => {
          window.console.log(error);
          loading();
          window.console.log(error)
          loading()
        }
      );
      )
    },
    rowDel(row) {
    rowDel (row) {
      this.$confirm('确定将选择数据删除?', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => {
          return remove(row.id);
          return remove(row.id)
        })
        .then(() => {
          this.onLoad(this.page);
          this.onLoad(this.page)
          this.$message({
            type: 'success',
            message: '操作成功!',
          });
        });
          })
        })
    },
    searchReset() {
      this.query = {};
      this.onLoad(this.page);
    searchReset () {
      this.query = {}
      this.onLoad(this.page)
    },
    searchChange(params, done) {
      this.query = params;
      this.page.currentPage = 1;
      this.onLoad(this.page, params);
      done();
    searchChange (params, done) {
      this.query = params
      this.page.currentPage = 1
      this.onLoad(this.page, params)
      done()
    },
    selectionChange(list) {
      this.selectionList = list;
    selectionChange (list) {
      this.selectionList = list
    },
    selectionClear() {
      this.selectionList = [];
      this.$refs.crud.toggleSelection();
    selectionClear () {
      this.selectionList = []
      this.$refs.crud.toggleSelection()
    },
    handleDelete() {
    handleDelete () {
      if (this.selectionList.length === 0) {
        this.$message.warning('请选择至少一条数据');
        return;
        this.$message.warning('请选择至少一条数据')
        return
      }
      this.$confirm('确定将选择数据删除?', {
        confirmButtonText: '确定',
@@ -1090,104 +1028,104 @@
        type: 'warning',
      })
        .then(() => {
          return remove(this.ids);
          return remove(this.ids)
        })
        .then(() => {
          this.onLoad(this.page);
          this.onLoad(this.page)
          this.$message({
            type: 'success',
            message: '操作成功!',
          });
          this.$refs.crud.toggleSelection();
        });
          })
          this.$refs.crud.toggleSelection()
        })
    },
    getFullAreaCode(areaCode) {
      if (!areaCode) return '';
    async getFullAreaCode (areaCode) {
      if (!areaCode) return ''
      const code = areaCode.toString();
      const code = areaCode.toString()
      if (code.includes(',')) return code;
      if (code.includes(',')) return code
      if (code.endsWith('0000000000')) {
        return code;
        return code
      } else if (code.endsWith('00000000') && !code.endsWith('0000000000')) {
        const provinceCode = code.substring(0, 2) + '0000000000';
        return `${provinceCode},${code}`;
        const provinceCode = code.substring(0, 2) + '0000000000'
        return `${provinceCode},${code}`
      } else {
        const provinceCode = code.substring(0, 2) + '0000000000';
        const cityCode = code.substring(0, 4) + '00000000';
        return `${provinceCode},${cityCode},${code}`;
        const provinceCode = code.substring(0, 2) + '0000000000'
        const cityCode = code.substring(0, 4) + '00000000'
        return `${provinceCode},${cityCode},${code}`
      }
    },
    beforeOpen(done, type) {
    beforeOpen (done, type) {
      if (['edit', 'view'].includes(type)) {
        getDetail(this.form.id).then(res => {
          const data = res.data.data;
        getDetail(this.form.id).then(async res => {
          const data = res.data.data
          this.form = {
            ...data,
            area_code: this.getFullAreaCode(data.area_code),
            area_code: await this.getFullAreaCode(data.area_code),
            area_name: this.form.area_name,
            cnmode_code: this.getDockModeText(this.form.mode_code),
            cnstatus:this.form.status === false ? '离线':'在线',
            cnstatus: this.form.status === false ? '离线' : '在线',
            duration_of_insurance: [data?.insure_start_time || '', data?.insure_expired_time || ''],
          };
          console.log('this.form', this.form);
          }
          done();
        });
          done()
        })
      }
    },
    currentChange(currentPage) {
      this.page.currentPage = currentPage;
    currentChange (currentPage) {
      this.page.currentPage = currentPage
    },
    sizeChange(pageSize) {
      this.page.pageSize = pageSize;
    sizeChange (pageSize) {
      this.page.pageSize = pageSize
    },
    refreshChange() {
      this.onLoad(this.page, this.query);
    refreshChange () {
      this.onLoad(this.page, this.query)
    },
    onLoad(page, params = {}) {
    onLoad (page, params = {}) {
      // this.closeAllWebsoket();
      const { releaseTimeRange } = this.query;
      const { releaseTimeRange } = this.query
      let values = {
        ...params,
        ...this.query,
      };
      }
      if (releaseTimeRange) {
        values = {
          ...values,
          releaseTime_datege: releaseTimeRange[0],
          releaseTime_datelt: releaseTimeRange[1],
        };
        values.releaseTimeRange = null;
        }
        values.releaseTimeRange = null
      }
      values['type'] = 1;
      this.loading = true;
      values['type'] = 1
      this.loading = true
      getList(page.currentPage, page.pageSize, values).then(res => {
        const data = res.data.data;
        this.page.total = data.total;
        const data = res.data.data
        this.page.total = data.total
        data.records.forEach(e => {
          e['hasChildren'] = e.has_children;
          e['hasChildren'] = e.has_children
          if (e.firmware_status == 4) {
            this.connectWebSocket(e);
            this.connectWebSocket(e)
          }
        });
        this.data = data.records;
        this.loading = false;
        this.selectionClear();
      });
        })
        this.data = data.records
        this.loading = false
        this.selectionClear()
      })
    },
    treeLoad(tree, treeNode, resolve) {
    treeLoad (tree, treeNode, resolve) {
      var params = {
        childSn: tree.child_sn,
      };
      }
      getList(1, 10, params).then(res => {
        const data = res.data.data.records;
        resolve(data);
      });
        const data = res.data.data.records
        resolve(data)
      })
    },
  },
};
}
</script>
<style lang="scss" scoped>
src/views/job/components/TaskIntermediateContent/TaskIntermediateContent.vue
@@ -113,6 +113,7 @@
import DeviceJobDetails from '../DeviceJobDetails.vue'
import CancelTaskDialog from '../CancelTaskDialog.vue'
import { useStore } from 'vuex'
import { cloneDeep } from 'lodash';
const store = useStore()
const singleUavHome = computed(() => store.state.home.singleUavHome)
const jobListParams = reactive({
@@ -129,7 +130,15 @@
// 获取任务列表
const getJobList = () => {
    jobList(jobListParams).then(res => {
  const apiParams = {
    ...cloneDeep(jobListParams),
    searchParams: {
      ...cloneDeep(jobListParams.searchParams),
      status_list: jobListParams.searchParams.status || undefined,
      status: undefined,
    },
  }
    jobList(apiParams).then(res => {
        if (res.data.code !== 0) return
        jobListData.value = res.data.data.records
        total.value = res.data.data.total
src/views/job/components/TaskTop/TaskEvent.vue
@@ -105,7 +105,12 @@
// 获取行业统计数据
const getJobEventBar = value => {
    jobEventBar(value).then(res => {
  const params = {
    ...value,
    status_list:value.status,
    status:undefined
  }
    jobEventBar(params).then(res => {
        if (res.data.code !== 0) return
        option.xAxis.data = res.data.data.map(item => item.name)
        option.series[0].data = res.data?.data.map(item => item.data[0].value)
src/views/job/components/TaskTop/TaskIndustry.vue
@@ -26,7 +26,7 @@
        left: 'center',
        bottom: '2%',
        textStyle: {
            color: '#6B90B5',
            color: '#363636',
            fontSize: 12,
        },
        itemWidth: 2, // 减小图例标记的宽度
src/views/tickets/orderLog.vue
@@ -90,9 +90,9 @@
          </div>
          <!-- 表格部分 -->
          <avue-crud :data="tableData" :option="option" v-model:page="page" ref="crud" :table-loading="loading"
            @current-change="currentChange" @refresh-change="refreshChange" @on-load="onLoad"
            @search-change="searchChange" @size-change="sizeChange" v-if="activeTab === tab.name">
          <avue-crud class="ztzf-public-general-avue-crud" :data="tableData" :option="option" v-model:page="page"
            ref="crud" :table-loading="loading" @current-change="currentChange" @refresh-change="refreshChange"
            @on-load="onLoad" @search-change="searchChange" @size-change="sizeChange" v-if="activeTab === tab.name">
            <template #menu-left>
              <el-button v-if="hasAddBtnPermission() && activeTab != 'WAIT_AUDIT'" type="primary" icon="el-icon-plus"
                @click="handleAdd">新建工单</el-button>
@@ -1197,6 +1197,7 @@
      this.page.currentPage = 1
      this.onLoad(this.page, this.query)
      this.handleViewDetail()
    },
    /**
     * 驳回
src/views/tickets/ticket.vue
@@ -31,9 +31,10 @@
          </div>
          <!-- 表格部分 -->
          <avue-crud ref="avueCrud" v-model="tableData" :option="option" :data="tableData" v-model:page="page"
            @size-change="sizeChange" @current-change="handleCurrentChange" :table-loading="loading"
            @selection-change="handleSelectionChange" :permission="permissionList" v-if="activeTab === tab.name">
          <avue-crud class="ztzf-public-general-avue-crud" ref="avueCrud" v-model="tableData" :option="option"
            :data="tableData" v-model:page="page" @size-change="sizeChange" @current-change="handleCurrentChange"
            @refresh-change="refreshChange" :table-loading="loading" @selection-change="handleSelectionChange"
            :permission="permissionList" v-if="activeTab === tab.name">
            <template #menu-left>
              <el-button v-if="(activeTab === 'all' || activeTab === 'myTickets') && permissionList.addBtn"
                type="primary" icon="el-icon-plus" @click="handleAdd">新建工单</el-button>
@@ -55,7 +56,7 @@
              <template v-if="permission.tickets_repeat_review">
                <el-button v-if="row.status === 4 && row.isReview !== 1" type="text" icon="el-icon-check"
                           @click="reCheck(row)">复核</el-button>
                  @click="reCheck(row)">复核</el-button>
              </template>
            </template>
@@ -124,10 +125,10 @@
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="选择位置" prop="location" required>
              <el-form-item label="选择位置" prop="location">
                <div class="location-wrapper">
                  <avue-input-map v-model="form.location" :params="mapParams" @change="handleLocationChange"
                    type="button">
                  <avue-input-map v-model="form.location" :clearable="false" :params="mapParams"
                    @change="handleLocationChange" type="button">
                    <el-button type="primary" plain class="map-button">
                      <i class="el-icon-map-location"></i> 地图选点
                    </el-button>
@@ -148,22 +149,23 @@
            </el-col>
            <el-col :span="12">
              <el-form-item label="附件图片" prop="photos" required class="upload-wrapper">
                <el-upload v-if="createoredit === 1" ref="upload" :action="'#'" :auto-upload="false" list-type="picture-card"
                  :on-change="handleFileChange" :on-remove="handleUploadRemove" :before-upload="beforeUpload"
                  :file-list="form.photos" :limit="1" accept="image/*" class="create-upload">
                <el-upload v-if="createoredit === 1" ref="upload" :action="'#'" :auto-upload="false"
                  list-type="picture-card" :on-change="handleFileChange" :on-remove="handleUploadRemove"
                  :before-upload="beforeUpload" :file-list="form.photos" :limit="1" accept="image/*"
                  class="create-upload">
                  <template v-if="form.photos.length < 1">
                    <!-- <i class="el-icon-plus">+</i> -->
                      <div class="el-icon-plus">
                    <div class="el-icon-plus">
                      <span>+</span>
                    </div>
                  </template>
                </el-upload>
                  <el-upload v-else ref="upload" :action="'#'" :auto-upload="false" list-type="picture-card"
                <el-upload v-else ref="upload" :action="'#'" :auto-upload="false" list-type="picture-card"
                  :on-change="handleFileChange" :on-remove="handleUploadRemove" :before-upload="beforeUpload"
                  :file-list="popupShowImage(form.photos)" :limit="1" accept="image/*" class="create-upload">
                  <template v-if="form.photos.length < 1">
                      <div class="el-icon-plus">
                    <div class="el-icon-plus">
                      <span>+</span>
                    </div>
                  </template>
@@ -186,8 +188,10 @@
    <!-- 工单详情对话框 -->
    <el-dialog v-model="detailVisible" title="工单详情" width="80%" append-to-body>
      <div class="detail-container">
        <div class="detail-top-title">  <div class="event-title-center event-orderNumber">{{ currentDetail.orderNumber || '工单编号' }}</div>
        <div class="event-title-center">{{ currentDetail.orderName || '事件名称' }}</div></div>
        <div class="detail-top-title">
          <div class="event-title-center event-orderNumber">{{ currentDetail.orderNumber || '工单编号' }}</div>
          <div class="event-title-center">{{ currentDetail.orderName || '事件名称' }}</div>
        </div>
        <div v-if="totalTime" class="event-total-time">
          总耗时:{{ totalTime }}
        </div>
@@ -321,8 +325,8 @@
            <template v-if="!currentDetail.photos || currentDetail.photos.length < 1">
              <!-- <i class="el-icon-plus">+</i> -->
              <div class="el-icon-plus">
                      <span>+</span>
                    </div>
                <span>+</span>
              </div>
            </template>
          </el-upload>
          <div class="el-upload__tip" v-if="hasProcessedAndOverBtnPermission()">
@@ -517,7 +521,7 @@
</template>
<script>
import { getSmallImg,getShowImg } from '@/utils/util';
import { getSmallImg, getShowImg } from '@/utils/util'
import { ElMessageBox, ElLoading } from 'element-plus'
import { calculateDefaultRange } from '@/utils/util'
import { gcj02ToWgs84, wgs84ToGcj02 } from '@/utils/coordinateTransformation'
@@ -553,7 +557,7 @@
      dispatchLoading: false,
      completeLoading: false,
      finalizeLoading: false,
      createoredit:'',
      createoredit: '',
      activeTab: 'all',
      // tabs 只保留静态结构,不做权限判断
      tabs: [
@@ -675,14 +679,17 @@
        algorithm: [{ required: true, message: '请选择关联算法', trigger: 'change' }],
        location: [
          {
            required: true,
            validator: (rule, value, callback) => {
              if (!value || value.length < 2) {
                callback(new Error('请选择位置信息'))
              } else if (!value[0] || !value[1]) {
                callback(new Error('请选择位置信息'))
              } else {
                callback()
              }
            },
            trigger: 'blur',
            trigger: 'change'
          },
        ],
        photos: [
@@ -737,6 +744,8 @@
    }
  },
  created () {
    this.inputMapShowDefaultCenter = null
    this.loadAMapScripts()
    this.fetchDropdownData()
    console.log('permission.tickets_processing_btn', this.permission.tickets_processing_btn)
@@ -745,7 +754,7 @@
  mounted () {
    const href = this.$route.href
    if (this.$route?.query?.status !== undefined && this.$route?.query?.status !== null){
    if (this.$route?.query?.status !== undefined && this.$route?.query?.status !== null) {
      this.filters.status = this.$route?.query?.status + ''
      this.$router.replace({})
    }
@@ -1038,19 +1047,21 @@
        // 直接从返回对象中获取正确的路径
        const center = adcodeObj?.payload?.objects?.collection?.geometries?.[0]?.properties?.center
        if (Array.isArray(center) && center.length === 2) {
          this.mapParams.center = center
          this.inputMapShowDefaultCenter = center
        } else {
          // 如果找不到中心点,尝试使用 bbox 的中心点
          const bbox = adcodeObj?.payload?.bbox
          if (Array.isArray(bbox) && bbox.length === 4) {
            const centerX = (bbox[0] + bbox[2]) / 2
            const centerY = (bbox[1] + bbox[3]) / 2
            this.mapParams.center = [centerX, centerY]
            this.inputMapShowDefaultCenter = [centerX, centerY]
          } else {
            this.mapParams.center = [115.861365, 28.621311]
            this.inputMapShowDefaultCenter = [115.861365, 28.621311]
          }
        }
        this.mapParams.center = [...this.inputMapShowDefaultCenter]
        this.mapLoaded = true
      } catch (error) {
@@ -1342,6 +1353,13 @@
          handlerValue = undefined
        }
        // 验证位置信息
        if (!this.form.location || this.form.location.length < 2 || !this.form.location[0] || !this.form.location[1]) {
          this.$message.warning('请在地图上选择位置')
          return
        }
        let [lng, lat] = this.disposeLocation(true, this.form)
        const submitData = {
@@ -1362,7 +1380,7 @@
        // 草稿时也至少需要工单名称
        if (!submitData.eventName) {
          this.$message.warning('请至少输入工单名称')
          this.$message.warning('请输入工单名称')
          return
        }
@@ -1380,18 +1398,18 @@
        if (response.data.code === 0) {
          this.$message.success('草稿保存成功')
          this.dialogVisible = false
          this.form={
        name: '',
        type: '',
        department: '',
        handler: '',
        algorithm: [], // 关联算法改为数组
        location: [], // 将存储为[经度, 纬度, 地址]格式
        address: '',
        photos: [],
        content: '', // 新增字段,用于存储后端返回的 content
      },
          this.fetchTableData()
          this.form = {
            name: '',
            type: '',
            department: '',
            handler: '',
            algorithm: [], // 关联算法改为数组
            location: [], // 将存储为[经度, 纬度, 地址]格式
            address: '',
            photos: [],
            content: '', // 新增字段,用于存储后端返回的 content
          },
            this.fetchTableData()
        } else {
          throw new Error(response.data.msg || '保存失败')
        }
@@ -1504,6 +1522,8 @@
    },
    async handleCurrentChange (val) {
      // 先更新页码
      this.page.currentPage = val
      // 等待 DOM 更新后再请求数据
@@ -1512,6 +1532,7 @@
    },
    async sizeChange (val) {
      this.page.pageSize = val
      this.page.currentPage = 1 // 重置到第一页
      await this.$nextTick()
@@ -1519,10 +1540,9 @@
    },
    handleAdd () {
    this.createoredit =1
      this.createoredit = 1
      this.dialogVisible = true
      this.mapParams.center = null
      this.mapParams.center = [...this.inputMapShowDefaultCenter]
      this.form.location = []
    },
@@ -1616,14 +1636,14 @@
          this.stepInfos = []
        }
      }
      console.log(detailData,'detailDatadetailDatadetailData');
      console.log(detailData, 'detailDatadetailDatadetailData')
      this.currentDetail = {
        ...detailData,
        address:null,
        latAndLon: _.round(detailData.location[0],6) + ',' + _.round(detailData.location[1],6)
        address: null,
        latAndLon: _.round(detailData.location[0], 6) + ',' + _.round(detailData.location[1], 6)
      }
       console.log('this.currentDetail',this.currentDetail);
      console.log('this.currentDetail', this.currentDetail)
      this.detailVisible = true
      this.$nextTick(() => {
        if (this.$refs.MapContainer && this.$refs.MapContainer.initAddEntity) {
@@ -2037,7 +2057,7 @@
    // 添加编辑方法
    handleEdit (row) {
    this.createoredit = 2
      this.createoredit = 2
      console.log('编辑原始数据:', row)
      // 尝试从row.dept_id或通过部门名称查找对应的部门ID
@@ -2113,7 +2133,7 @@
        address: row.address || '',
        content: row.content,
        photos: [],
      }
      let curLocation = []
@@ -2130,7 +2150,7 @@
      if (Array.isArray(this.form.location) && this.form.location.length >= 2) {
        this.mapParams.center = [Number(this.form.location[0]), Number(this.form.location[1])]
      } else {
        this.mapParams.center = null // 没有经纬度时用默认
        this.mapParams.center = [...this.inputMapShowDefaultCenter]
      }
      // 如果有图片,添加到表单中
      if (row.photo_url) {
@@ -2187,6 +2207,7 @@
    // 添加选择变化处理方法
    handleSelectionChange (selection) {
      this.selections = selection
      console.log('已选择的行:', selection)
    },
@@ -2437,20 +2458,23 @@
     * @param goWgs84 是否由国测转换到84,默认false----由84转换到国测
     */
    disposeLocation (goWgs84 = false, data) {
      let lng, lat
      if (goWgs84) {
        lng = data.location?.[0] ? String(data.location[0]) : undefined
        lat = data.location?.[1] ? String(data.location[1]) : undefined
      let lng = '', lat = ''
        if (lng && lat) {
          [lng, lat] = gcj02ToWgs84(Number(lng), Number(lat))
        }
      } else {
        lng = Number(data.location[0])
        lat = Number(data.location[1])
      if (Array.isArray(data.location) && data.location.length > 0) {
        if (goWgs84) {
          lng = data.location?.[0] ? String(data.location[0]) : undefined
          lat = data.location?.[1] ? String(data.location[1]) : undefined
        if (lng && lat) {
          [lng, lat] = wgs84ToGcj02(Number(lng), Number(lat))
          if (lng && lat) {
            [lng, lat] = gcj02ToWgs84(Number(lng), Number(lat))
          }
        } else {
          lng = Number(data.location[0])
          lat = Number(data.location[1])
          if (lng && lat) {
            [lng, lat] = wgs84ToGcj02(Number(lng), Number(lat))
          }
        }
      }
@@ -2811,12 +2835,15 @@
.detail-container {
  padding: 20px;
  .detail-top-title {
  display: flex;
  justify-content: center;
  align-items: center;
  .event-orderNumber{
  margin-right: 10px;}
    display: flex;
    justify-content: center;
    align-items: center;
    .event-orderNumber {
      margin-right: 10px;
    }
  }
}
src/views/wel/components/backlog.vue
@@ -26,12 +26,13 @@
                checked == '智飞工单' ? zfstatusMap[item.status] : statusMap[item.status]
              }}</span>
              <span class="reads" :class="item.is_read === 0 ? 'unread' : ''"></span>
              <span class="todo-text">{{ item.name }}</span>
              <span class="todo-text">{{ item.event_num }}</span>
            </div>
            <div class="action-area">
              <img :src="st7" alt="" />
              <span class="todo-date">{{ item.date?.slice(0, 10).replace(/-/g, '.') }}</span>
              <!-- <img :src="st7" alt="" /> -->
              <!-- <span class="todo-date">{{ item.date?.slice(0, 10).replace(/-/g, '.') }}</span> -->
          <span class="todo-date">{{ item.name }}</span>
            </div>
          </div>
        </div>
@@ -183,6 +184,8 @@
    loading.value = true
    const type = 1
    const res = await addOrderRecordApi(type, ticketId)
  } else if (checked.value === '智飞工单') {
    loading.value = true
    const type = 2
@@ -207,12 +210,14 @@
    const res = await getOrderOrEventApi(type, userInfo.value.detail.areaCode)
    todos.value = res.data.data || []
    loading.value = false
      //  console.log('事件工单', todos.value);
  } else if (checked.value === '智飞工单') {
    loading.value = true
    const type = 2
    const res = await getOrderOrEventApi(type, userInfo.value.detail.areaCode)
    todos.value = res.data.data || []
    loading.value = false
      //  console.log('智飞工单', todos.value);
  }
}
@@ -366,7 +371,7 @@
        .todo-text {
          font-weight: 400;
          font-size: 14px;
          color: #343434;
         color: #5C6476;
        }
        .unread {
@@ -398,8 +403,8 @@
        .todo-date {
          font-weight: 400;
          font-size: 14px;
          color: #5c6476;
        font-size: 14px;
color: #343434;
          margin-left: 2px;
        }
      }
src/views/wel/components/calendarBox.vue
@@ -5,10 +5,15 @@
        <div :class="data.isSelected ? 'is-selected' : ''">
          <div class="date-number">{{ data.day.slice(8, 10) }}</div>
          <div class="events">
            <div v-for="(event, index) in getEvents(data.day)" :key="index" class="event-item" :class="event.type"
              @click="jumpcalendar(event, data.day)">
              <img :src="getEventIcon(event.type)" alt="" />
              {{ event.name }}<span>{{ event.value }}</span>
            <div
              v-for="(event, index) in getEvents(data.day)"
              :key="index"
              class="event-item"
              :class="event.type"
              @click="jumpcalendar(event, data.day)"
            >
              <div class="imgBox"><img :src="getEventIcon(event.type)" alt="" /> {{ event.name }}</div>
              <span>{{ event.value }}</span>
            </div>
          </div>
        </div>
@@ -18,65 +23,71 @@
</template>
<script setup>
import dayjs from 'dayjs'
import { jobEventBar, getCalen } from '@/api/home/index'
import { useRouter } from 'vue-router'
import ev1 from '@/assets/images/workbench/ev1.svg'
import ev2 from '@/assets/images/workbench/ev2.svg'
import { ElMessage } from 'element-plus'
const router = useRouter()
const events = ref({})
import dayjs from 'dayjs';
import { jobEventBar, getCalen } from '@/api/home/index';
import { useRouter } from 'vue-router';
import ev1 from '@/assets/images/workbench/ev1.svg';
import ev2 from '@/assets/images/workbench/ev2.svg';
import { ElMessage } from 'element-plus';
const router = useRouter();
const events = ref({});
const params = ref({
  end_date: undefined,
  start_date: undefined,
})
});
const eventIcons = ref({
  'work-order': ev1,
  task: ev2,
})
});
const getEventIcon = type => {
  return eventIcons.value[type] || eventIcons.value.default
}
function getCurrentMonthRange () {
  return eventIcons.value[type] || eventIcons.value.default;
};
function getCurrentMonthRange() {
  return {
    start_date: dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss'),
    end_date: dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss'),
  }
  };
}
const leftValue = ref(new Date())
const leftValue = ref(new Date());
watch(
  () => leftValue.value,
  (newV, oldV) => {
    if (newV && oldV) {
      const newDate = dayjs(newV)
      const newDate = dayjs(newV);
      params.value = {
        start_date: newDate.startOf('month').format('YYYY-MM-DD HH:mm:ss'),
        end_date: newDate.endOf('month').format('YYYY-MM-DD HH:mm:ss'),
      }
      getJobEventBar()
      };
      getJobEventBar();
    }
  },
  { deep: true, immediate: true }
)
);
// 获取日期数字
const getDate = date => {
  return date.getDate()
}
  return date.getDate();
};
// 获取对应日期的事件
const getEvents = dateString => {
  return events.value[dateString] || []
}
const monthRange = getCurrentMonthRange()
params.value = monthRange
  return events.value[dateString] || [];
};
const monthRange = getCurrentMonthRange();
params.value = monthRange;
const getJobEventBar = () => {
  getCalen(params.value).then(res => {
    if (res.data.code !== 0) return
    events.value = res.data.data
  })
}
    if (res.data.code !== 0) return;
    const a = res.data.data;
    const filteredData = {};
    for (let date in a) {
      filteredData[date] = a[date].filter(item => item.type !== 'work-order');
    }
    events.value = filteredData;
    // events.value = res.data.data
  });
};
const jumpcalendar = (event, day) => {
  if (event.name === '工单') {
    router.push({
@@ -84,32 +95,30 @@
      query: {
        day: day,
      },
    })
    });
  } else {
    router.push({
      path: '/job/jobstatistics',
      query: {
        day: day,
      },
    })
    });
  }
}
};
onMounted(() => {
  getJobEventBar()
})
  getJobEventBar();
});
</script>
<style lang="scss">
.calenBox {
  //  height: 630px;
  height: pxToVh(660);
  // 隐藏按钮组中间按钮
  .el-button-group>.el-button:not(:first-child):not(:last-child) {
  .el-button-group > .el-button:not(:first-child):not(:last-child) {
    display: none;
  }
  .date-number {
  font-size: 13px !important;}
}
</style>
@@ -123,7 +132,6 @@
  background-color: #fff;
  ::v-deep(.el-calendar) {
    // height: 80%; // 日历填充容器
    // 标题样式
    .el-calendar__title {
@@ -141,9 +149,10 @@
    //   }
    // }
    .el-calendar-table .el-calendar-day {
      height: 78px !important;
       height: pxToVh(91) !important;
    }
    // 选中日期样式
    .el-calendar-table td.is-selected {
      background-color: #f0f7ff;
@@ -160,7 +169,7 @@
  .event-item {
    font-size: 12px;
    // padding: 2px;
    text-align: center;
    border-radius: 3px;
    white-space: nowrap;
@@ -184,11 +193,13 @@
      span {
        font-weight: 600;
        font-size: 14px;
        font-size: 18px;
        color: #029d36;
        margin-left: 2px;
      }
    }
    .imgBox {
   font-size: 12px;}
  }
}
</style>
src/views/wel/components/flightStatistics.vue
New file
@@ -0,0 +1,446 @@
<template>
  <div class="flyOrder">
    <div class="fytitle">
      <div class="cardtotal">
        <p>飞行统计</p>
        <img @click="refresh" src="/src/assets/images/workbench/st1.svg" alt="" />
      </div>
      <div class="time-card">
        <div
          class="card-item"
          :class="item === checked ? 'active' : ''"
          v-for="(item, index) in timeListEnum"
          :key="index"
          @click="timeClick(item, index)"
        >
          {{ timeListStr[index] }}
        </div>
      </div>
    </div>
    <div class="flycenter">
      <div class="centerBox">
        <div class="centerItem" v-for="(itemfly, index) in flyTypeList" :key="index">
          <div class="flyimg"><img :src="itemfly.img" alt="" />{{ itemfly.name }}</div>
          <div class="flydata">
            <span>{{ itemfly.value }}</span
            >{{ unitMap[itemfly.name] }}
          </div>
        </div>
      </div>
      <div class="lineChart">
        <div class="lineBox" ref="chartRef"></div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { getFly, getFlyTime, tongjiNestApi } from '@/api/home/index';
import flyImg1 from '@/assets/images/workbench/fy2.svg';
import flyImg2 from '@/assets/images/workbench/fy3.svg';
import flyImg3 from '@/assets/images/workbench/fy4.svg';
import * as echarts from 'echarts';
import useEchartsResize from '@/hooks/useEchartsResize';
let checked = ref('CURRENT_YEAR');
let timeListStr = ['本周', '本月', '本年'];
let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR'];
const dateSelect = ref('CURRENT_YEAR');
const params = ref({
  date_enum: 'CURRENT_YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
});
const flyparams = ref({
  date_enum: 'YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
});
const chartRef = ref(null);
let { chart: lineChart } = useEchartsResize(chartRef);
const keyMapping = {
  飞行时长: 'total_flight_time',
  飞行里程: 'total_flight_distance',
  //   任务成果: 'event_num',
};
const unitMap = {
  飞行时长: '小时',
  飞行里程: '公里',
  //   任务成果: '个',
};
const flyTypeList = ref([
  { name: '飞行时长', value: 0, img: flyImg1, key: 'hour_count' },
  { name: '飞行里程', value: 0, img: flyImg2, key: 'flight_mileage' },
  //   { name: '任务成果', value: 0, img: flyImg3 },
]);
// 飞行统计
const getFlyList = () => {
  // getFly(params.value).then(res => {
  //   flyTypeList.value = flyTypeList.value.map(item => ({
  //     ...item,
  //     value: res.data.data[keyMapping[item.name]] || 0,
  //   }));
  // });
  tongjiNestApi(flyparams.value).then(res => {
    flyTypeList.value = flyTypeList.value.map(item => ({
      ...item,
      value: res.data.data[item.key] || 0,
    }));
  });
};
const getFlyTimeList = () => {
  getFlyTime(params.value).then(res => {
    const resList = res?.data?.data || [];
    lineCharts(resList);
  });
};
const refresh = () => {
  params.value.date_enum = 'CURRENT_YEAR';
  checked.value = 'CURRENT_YEAR';
  dateSelect.value = 'CURRENT_YEAR';
  getFlyList();
  getFlyTimeList();
};
let timeClick = (item, index) => {
  checked.value = item;
  params.value.date_enum = item;
  dateSelect.value = item;
  if (item === 'CURRENT_MONTH') {
    flyparams.value.date_enum = 'MONTH';
  } else if (item === 'CURRENT_WEEK') {
    flyparams.value.date_enum = 'WEEK';
  } else {
    flyparams.value.date_enum = 'YEAR';
  }
  getFlyList();
  getFlyTimeList();
};
// 折线/柱状图
const lineCharts = bardata => {
  const categories = bardata?.map(item => item.name); // x轴类别
  const flight_distance = [];
  const flight_time = [];
  const event_num = [];
  // /遍历数据填充各系列
  bardata?.forEach(period => {
    let hasDuration = false,
      hasDistance = false,
      hasResult = false;
    period.data?.forEach(item => {
      switch (item.name) {
        case '飞行时长':
          flight_time.push(parseFloat(item.value) || 0);
          hasDuration = true;
          break;
        case '飞行里程':
          flight_distance.push(parseFloat(item.value) || 0);
          hasDistance = true;
          break;
      }
    });
    // 处理可能缺失的数据项
    if (!hasDuration) flight_time.push(0);
    if (!hasDistance) flight_distance.push(0);
  });
  var option = {
    tooltip: {
      trigger: 'item',
      axisPointer: {
        type: 'shadow',
      },
    },
    grid: {
      left: '2%',
      right: '4%',
      bottom: '14%',
      top: '16%',
      containLabel: true,
    },
    legend: {
      data: ['飞行时长', '飞行里程'],
      left: 'center',
      top: '5%',
      color: '#383838', // 直接配置颜色
      itemWidth: 15,
      itemHeight: 10,
      itemGap: 25,
      fontSize: '1.2rem',
    },
    xAxis: {
      type: 'category',
      data: categories,
      axisLine: {
        lineStyle: {
          color: '#cdd5e2',
        },
      },
      axisLabel: {
        interval: 0,
        color: '#383838',
      },
    },
    yAxis: [
      {
        type: 'log',
        name: '单位:小时',
        nameTextStyle: {
          color: '#383838',
          fontSize: '1.2rem',
        },
        min: 1,
        logBase: 3,
        axisLine: {
          show: false,
          lineStyle: {
            color: '#cdd5e2',
          },
        },
        splitLine: {
          show: true,
          lineStyle: {
            type: 'dashed',
            color: '#cdd5e2',
            width: 1,
            opacity: 0.5,
          },
        },
        axisLabel: {
          color: '#666666', // 直接配置颜色
        },
      },
      {
        type: 'log',
        name: '单位:公里',
        nameTextStyle: {
          color: '#383838',
        },
        min: 1,
        logBase: 3,
        axisLine: {
          show: false,
          lineStyle: {
            color: '#cdd5e2',
          },
        },
        splitLine: {
          show: false,
          lineStyle: {
            type: 'dashed',
            color: '#cdd5e2',
            width: 1,
            opacity: 0.5,
          },
        },
        axisLabel: {
          color: '#383838', // 直接配置颜色
        },
      },
    ],
    series: [
      {
        name: '飞行时长',
        type: 'bar',
        barWidth: '8px',
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: '#91C1FF' },
            { offset: 1, color: '#2970FF' },
          ]),
          borderRadius: 4,
        },
        data: flight_time,
      },
      {
        name: '飞行里程',
        type: 'line',
        yAxisIndex: 1,
        smooth: true,
        itemStyle: {
          color: '#01D0A7',
        },
        lineStyle: {
          width: 2,
          type: 'solid',
        },
        symbol: 'circle',
        symbolSize: 6,
        lineStyle: {
          color: '#01D0A7',
        },
        data: flight_distance,
      },
    ],
  };
  lineChart.value.setOption(option);
};
onMounted(() => {
  lineCharts();
  getFlyList();
  getFlyTimeList();
});
</script>
<style scoped lang="scss">
// 飞行统计
.flyOrder {
  border-radius: 8px 8px 8px 8px;
  padding: 4px 14px 0 15px;
  background: #ffffff !important;
  margin-top: 5px;
    height: pxToVh(400);
  .fytitle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    .cardtotal {
      display: flex;
      align-items: center;
      margin-left: 9px;
      img {
        width: 17px;
        height: 17px;
        margin-left: 6px;
        cursor: pointer;
      }
      p {
        font-weight: bold;
        font-size: 14px;
        color: #363636;
      }
      span {
        font-weight: 400;
        font-size: 14px;
        color: #7c8091;
      }
      .total-number {
        font-family: 'Source Han Sans CN';
        font-weight: bold;
        font-size: 32px;
        color: #2a54ff;
      }
    }
    img {
      width: 39px;
      height: 38px;
    }
    span {
      font-weight: bold;
      font-size: 14px;
      color: #363636;
    }
  }
  .flycenter {
    margin-top: 13px;
    .centerBox {
      display: flex;
      justify-content: space-between;
      margin-bottom: 10px;
      .centerItem {
        padding: 7px 7px 0 15px;
        width: 196px;
        flex: 1;
        // height: 80px;
        height: pxToVh(80);
        background: #f6f8fe;
        box-shadow: 0px 5px 4px 0px #ebf1ff;
        border-radius: 8px 8px 8px 8px;
        border: 1px solid #ffffff;
        margin-right: 16px;
        img {
          width: 15px;
          height: 15px;
          margin-right: 5px;
        }
        .flyimg {
          display: flex;
          align-items: center;
          font-weight: 400;
          font-size: 14px;
          color: #7c8091;
        }
        .flydata {
          margin-top: 10px;
          text-align: right;
          font-size: 14px;
          color: #7c8091;
          span {
            font-weight: bold;
            font-size: 24px;
            color: #343434;
            margin-right: 6px;
          }
        }
      }
    }
    .lineChart {
     height: pxToVh(233) ;
      width: 100%;
      .lineBox {
        width: 100%;
        height: 100%;
      }
    }
  }
}
.time-card {
  text-align: center;
  // height: 30px;
  height: pxToVh(30);
  background: #ffffff;
  border: 1px solid #e5e5e5;
  font-weight: 400;
  font-size: 14px;
  color: #7c8091;
  display: flex;
  width: 282px;
  margin-left: 15px;
  border-radius: 4px;
  .card-item {
    width: 94px;
    height: 100%;
    line-height: 28px;
    cursor: pointer;
    font-family: 'Source Han Sans CN';
    font-weight: 400;
    font-size: 14px;
    color: #7c8091;
  }
  .card-item:first-child {
    border-right: 1px solid #e5e5e5;
  }
  .card-item:nth-child(2) {
    border-right: 1px solid #e5e5e5;
  }
  .card-item.active {
    color: #1441ff;
    border: 1px solid #1c5cff;
  }
}
</style>
src/views/wel/components/flyratio.vue
@@ -2,10 +2,22 @@
  <div class="machineNest">
    <div class="nestTop">
      <div class="card-title">
        <img :src="jc1" alt="" />
        <div class="cardtotal">
          <p>机巢工单数量排名</p>
           <img @click="refresh" src="/src/assets/images/workbench/st1.svg" alt="" />
        </div>
         <div class="time-card">
        <div
          class="card-item"
          :class="item === checked ? 'active' : ''"
          v-for="(item, index) in timeListEnum"
          :key="index"
          @click="timeClick(item, index)"
        >
          {{ timeListStr[index] }}
        </div>
      </div>
      </div>
    </div>
    <div class="nestCenter">
@@ -21,7 +33,16 @@
import jc2 from '@/assets/images/workbench/jc2.png';
import { industryJobNumPieChart } from '@/api/home/index';
import { nextTick } from 'vue';
let checked = ref('CURRENT_YEAR');
let timeListStr = ['本周', '本月', '本年'];
let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR'];
const params = ref({
  date_enum: 'CURRENT_YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
});
const dateSelect = ref('CURRENT_YEAR');
const echartsRef = ref(null);
let { chart: jcchart } = useEchartsResize(echartsRef);
const jcOrder = ref([]);
@@ -30,9 +51,18 @@
    type: String,
  },
});
const params = reactive({
  date_enum: props.dateSelect,
});
const refresh = () => {
  params.value.date_enum = 'CURRENT_YEAR';
  checked.value = 'CURRENT_YEAR';
  dateSelect.value = 'CURRENT_YEAR';
getIndustryJobNumPieChart();
};
let timeClick = (item, index) => {
  checked.value = item;
  params.value.date_enum = item;
  dateSelect.value = item;
getIndustryJobNumPieChart();
};
// 空数据计算
const isEmptyData = computed(() => {
  return jcOrder.value.length === 0 ||
@@ -46,7 +76,7 @@
}
// 获取机巢事件数据
const getIndustryJobNumPieChart = () => {
  industryJobNumPieChart(params).then(res => {
  industryJobNumPieChart(params.value).then(res => {
    const resList = res?.data?.data || [];
    jcOrder.value = resList;
     nextTick(() => {
@@ -100,21 +130,22 @@
    },
    grid: {
      left: '3%',
      right: '4%',
      right: '3%',
      bottom: '3%',
         top: '-3%', // 添加顶部间距
      containLabel: true,
    },
    xAxis: {
      type: 'value',
      splitLine: { lineStyle: { color: '#E5E5E5' } },
      axisLabel: { color: '#7C8091',fontSize:'1.2rem' },
      splitLine: { lineStyle: { color: '#E5E5E5' },show:false },
      axisLabel: { color: '#7C8091',fontSize:'1.2rem',show:false },
      boundaryGap: [0, 0.01],
    },
    yAxis: {
      type: 'category',
      data: optionData.yAxisData,
      axisLabel: { color: '#7C8091' ,fontSize:'1.2rem'},
      axisLine: { lineStyle: { color: '#D1D1D1' } },
      axisLabel: { color: '#7C8091' ,fontSize:'1.2rem',},
      axisLine: { lineStyle: { color: '#D1D1D1' } ,show:false},
      axisTick: { show: false },
      // 确保排序后的数据从上到下显示(最大值在顶部)
      inverse: true,
@@ -125,6 +156,7 @@
        type: 'bar',
        data: optionData.seriesData,
        itemStyle: {
        barBorderRadius: [0, 20, 20, 0],
          color: params => {
            // 前6项使用固定颜色循环,之后使用渐变
            return params.dataIndex < colors.length
@@ -140,6 +172,11 @@
          show: true,
          position: 'right',
          formatter: '{c}',
          itemStyle:{
             fontSize: '1.4rem',
             color: '#414243',
          }
        },
      },
    ],
@@ -161,22 +198,58 @@
<style scoped lang="scss">
.machineNest {
  width: 100%;
  width: 93%;
  margin-left: 10px;
  height: pxToVh(355);
    border-radius: 8px 8px 8px 8px;
  padding: 4px 14px 0 15px;
  background: #ffffff !important;
  .custom-text {
    font-size: 14px;
    color: #7c8091;
  }
  .nestTop {
    .card-title {
   .card-title {
    display: flex;
    margin-bottom: 10px;
    align-items: center;
    justify-content: space-between;
    .cardtotal {
      display: flex;
      margin-left: 57px;
      margin-bottom: 15px;
      align-items: center;
      img {
        width: 36px;
        height: 40px;
        width: 17px;
        height: 17px;
        margin-left: 6px;
        cursor: pointer;
      }
      p {
        font-weight: bold;
        font-size: 14px;
        color: #363636;
      }
      span {
        font-weight: 400;
        font-size: 14px;
        color: #7c8091;
      }
      .total-number {
        font-family: 'Source Han Sans CN';
        font-weight: bold;
        font-size: 32px;
        color: #2a54ff;
      }
    }
    img {
      width: 36px;
      height: 40px;
    }
  }
    .cardtotal {
      display: flex;
      align-items: center;
@@ -231,11 +304,50 @@
  .nestCenter {
    width: 100%;
    // height: 600px;
    height: pxToVh(600);
    height: pxToVh(266);
    .chart {
      width: 100%;
      height: 100%;
    }
  }
}
.time-card {
  text-align: center;
  // height: 30px;
  height: pxToVh(30);
  background: #ffffff;
  border: 1px solid #e5e5e5;
  font-weight: 400;
  font-size: 14px;
  color: #7c8091;
  display: flex;
  width: 282px;
  margin-left: 15px;
  border-radius: 4px;
  .card-item {
    width: 94px;
    height: 100%;
    line-height: 28px;
    cursor: pointer;
    font-family: 'Source Han Sans CN';
    font-weight: 400;
    font-size: 14px;
    color: #7c8091;
  }
  .card-item:first-child {
    border-right: 1px solid #e5e5e5;
  }
  .card-item:nth-child(2) {
    border-right: 1px solid #e5e5e5;
  }
  .card-item.active {
    color: #1441ff;
    border: 1px solid #1c5cff;
  }
}
</style>
src/views/wel/components/proportionStatic.vue
New file
@@ -0,0 +1,362 @@
<template>
  <div class="workOrder">
    <div class="card-title">
      <div class="cardtotal">
        <p>工单统计占比</p>
        <img @click="refresh" src="/src/assets/images/workbench/st1.svg" alt="" />
      </div>
      <div class="time-card">
        <div
          class="card-item"
          :class="item === checked ? 'active' : ''"
          v-for="(item, index) in timeListEnum"
          :key="index"
          @click="timeClick(item, index)"
        >
          {{ timeListStr[index] }}
        </div>
      </div>
    </div>
    <div class="workorderbox">
      <div class="card-group">
        <div class="main-card">
          <div class="status-grid">
            <div class="status-item" v-for="(item, index) in eventTypeList" :key="index">
              <div class="statusCon">
                <div class="status-label">{{ item.name }}</div>
                <div class="status-value">{{ item.value }}<span>个</span></div>
                <div class="ratio">
                  占比
                  <span :style="{ color: item.color }"
                    >{{ ((item.rate * 100) / 100).toFixed(2) }}%</span
                  >
                </div>
              </div>
              <img :src="item.img" alt="" />
            </div>
          </div>
        </div>
      </div>
      <div class="charts">
        <div class="chart" ref="echartsRef"></div>
      </div>
    </div>
  </div>
</template>
<script setup>
import useEchartsResize from '@/hooks/useEchartsResize';
import { getJobEventByStatus } from '@/api/home/index';
import overviewImg2 from '@/assets/images/workbench/tc2.svg';
import overviewImg3 from '@/assets/images/workbench/tc3.svg';
import overviewImg4 from '@/assets/images/workbench/tc4.svg';
import overviewImg5 from '@/assets/images/workbench/tc5.svg';
let checked = ref('CURRENT_YEAR');
let timeListStr = ['本周', '本月', '本年'];
let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR'];
const params = ref({
  date_enum: 'CURRENT_YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
});
const dateSelect = ref('CURRENT_YEAR');
const eventTypeList = ref([
  { name: '待审核', value: 0, img: overviewImg2, color: '#FF472F', status: '2', rate: 0 },
  { name: '待处理', value: 0, img: overviewImg3, color: '#FF7411', status: '0', rate: 0 },
  { name: '处理中', value: 0, img: overviewImg4, color: '#FFC300', status: '3', rate: 0 },
  { name: '已完成', value: 0, img: overviewImg5, color: '#0291A1', status: '4', rate: 0 },
]);
//  工单统计
const getTypeData = () => {
  getJobEventByStatus(params.value).then(res => {
    const resList = res?.data?.data || [];
    resList.forEach(item => {
      eventTypeList.value.forEach(item1 => {
        if (item1.name === item.name) {
          item1.value = item.num;
          item1.rate = item.rate;
        }
      });
    });
    initChart(resList);
  });
};
const refresh = () => {
  params.value.date_enum = 'CURRENT_YEAR';
  checked.value = 'CURRENT_YEAR';
  dateSelect.value = 'CURRENT_YEAR';
  getTypeData();
};
let timeClick = (item, index) => {
  checked.value = item;
  params.value.date_enum = item;
  dateSelect.value = item;
  getTypeData();
};
// 图表
const echartsRef = ref(null);
let { chart } = useEchartsResize(echartsRef);
const initChart = val => {
  const totalNum = val.reduce((sum, item) => sum + item.num, 0);
  const data = {
    total: {
      title: '总计',
      figure: totalNum.toString(), // 动态计算总数
    },
    data: val.map(item => ({
      value: item.num,
      name: item.name,
      rate: item.rate,
    })),
  };
  const containerWidth = chart.value.clientWidth;
  const isSmallScreen = containerWidth < 768; // 移动端判断
  const echartsOption = {
    color: ['#FF472F', '#FF7411', '#FFC300', '#0291A1'],
    tooltip: {
      trigger: 'item',
      padding: 0,
      borderWidth: 0,
      formatter: params => {
        return `<div style="background-color: rgba($color: #FFFFFF, $alpha: 0.95);
          box-shadow: 0 0.4rem 0.9rem 0 rgba($color: #000000, $alpha: 0.1);
          padding:0 1.2rem;
          display: flex;
          align-items: center;
          border-radius: 0.4rem;
          font-size: 1.2rem;" class="tooltip-area">
          <p>${params.marker}${params.name}</p>
          <h4 style="margin-left: 1rem;">${params.data.rate || params.percent}%</h4>
        </div>`;
      },
    },
    legend: {
      show: false,
    },
    title: {
      text: data.total.title,
      textStyle: {
        color: 'rgba(28, 31, 35, 0.80)',
        fontSize: '1.2rem',
        fontWeight: 'bold',
      },
      subtext: data.total.figure,
      subtextStyle: {
        color: '#1C1F23',
        fontSize: '2rem',
        fontWeight: '600',
      },
      top: '40%',
      left: '48%',
      textAlign: 'center', // 文本对齐
    },
    series: [
      {
        name: '',
        type: 'pie',
        radius: ['55%', '83%'],
        label: {
          show: false,
        },
        labelLine: {
          show: false,
          length: 3, // 调整引导线长度
          length2: 5,
          lineStyle: {
            cap: 'round',
          },
          minTurnAngle: 45, // 防止小角度重叠
        },
        data: data.data,
      },
    ],
  };
  chart.value.setOption(echartsOption);
};
onMounted(() => {
  getTypeData();
});
</script>
<style scoped lang="scss">
.workOrder {
  border-radius: 8px 8px 8px 8px;
  padding: 4px 14px 0 15px;
  background: #ffffff !important;
//   height: 315px;
    height: pxToVh(355);
  margin-bottom: 10px;
  .card-title {
    display: flex;
    margin-bottom: 10px;
    align-items: center;
    justify-content: space-between;
    .cardtotal {
      display: flex;
      align-items: center;
      margin-left: 9px;
      img {
        width: 17px;
        height: 17px;
        margin-left: 6px;
        cursor: pointer;
      }
      p {
        font-weight: bold;
        font-size: 14px;
        color: #363636;
      }
      span {
        font-weight: 400;
        font-size: 14px;
        color: #7c8091;
      }
      .total-number {
        font-family: 'Source Han Sans CN';
        font-weight: bold;
        font-size: 32px;
        color: #2a54ff;
      }
    }
    img {
      width: 36px;
      height: 40px;
    }
  }
  .workorderbox {
    display: flex;
    justify-content: space-between;
    .card-group {
      width: 60%;
      .status-grid {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        // row-gap: 19px;
        // gap: 10px;
        // padding-bottom: 5px;
        .status-item {
          display: flex;
          text-align: center;
          justify-content: space-between;
        //   height: 97px;
          height: pxToVh(107);
          //   max-width: 158px;
          margin-bottom: 19px;
          margin-right: 14px;
          width: 144px;
          background: #f6f8fe;
          border-radius: 8px 8px 8px 8px;
          img {
            width: 26px;
            height: 26px;
            padding: 9px 10px 9px 2px;
          }
          .statusCon {
            display: flex;
            flex-direction: column;
            // align-items: center;
            padding: 5px 4px 9px 10px;
            text-align: left;
            .status-label {
              font-weight: 400;
              font-size: 14px;
              text-align: left;
              color: #383838;
            }
            .ratio {
              font-weight: 400;
              font-size: 12px;
              color: #363636;
              white-space: nowrap;
              text-align: left;
            }
            .status-value {
              font-family: 'Source Han Sans CN';
              font-weight: bold;
              font-size: 30px;
              color: #363636;
              // margin: 5px 0;
              // font-style: italic;
              display: inline-block;
              transform: skewX(-5deg);
              span {
                font-weight: 400;
                font-size: 14px;
                color: #7c8091;
              }
            }
          }
        }
      }
    }
    .charts {
      margin-top: 20px;
      width: 40%;
      height: auto;
      .chart {
        width: 100%;
        height: 100%;
      }
    }
  }
}
.time-card {
  text-align: center;
  // height: 30px;
  height: pxToVh(30);
  background: #ffffff;
  border: 1px solid #e5e5e5;
  font-weight: 400;
  font-size: 14px;
  color: #7c8091;
  display: flex;
  width: 282px;
  margin-left: 15px;
  border-radius: 4px;
  .card-item {
    width: 94px;
    height: 100%;
    line-height: 28px;
    cursor: pointer;
    font-family: 'Source Han Sans CN';
    font-weight: 400;
    font-size: 14px;
    color: #7c8091;
  }
  .card-item:first-child {
    border-right: 1px solid #e5e5e5;
  }
  .card-item:nth-child(2) {
    border-right: 1px solid #e5e5e5;
  }
  .card-item.active {
    color: #1441ff;
    border: 1px solid #1c5cff;
  }
}
</style>
src/views/wel/components/statistics.vue
@@ -67,7 +67,7 @@
          </template>
          <template v-else-if="test[item]?.name == '机巢保险'">
            <div
              v-for="statusKey in [1, 0]"
              v-for="statusKey in [1, 0,2]"
              :key="statusKey"
              class="status-item"
              :class="getStatusStyle(test[item]?.name, statusKey)"
@@ -84,6 +84,28 @@
              <span class="count">
                {{ newtitleData[item].status_map[statusKey] }}
                {{ test[item]?.name === '无人机' ? '架' : '个' }}
              </span>
            </div>
          </template>
            <template v-else-if="test[item]?.name == '无人机流量'">
            <div
              v-for="statusKey in [ 0,2,1]"
              :key="statusKey"
              class="status-item"
              :class="getStatusStyle(test[item]?.name, statusKey)"
              :style="{ color: getStatusColor(test[item]?.name, statusKey) }"
            >
              <span
                class="indicator"
                :style="{
                  backgroundColor: getStatusBackground(test[item]?.name, statusKey),
                  color: getStatusColor(test[item]?.name, statusKey),
                }"
              ></span>
              <span class="label">{{ getStatusLabel(test[item]?.name, statusKey) }}</span>
              <span class="count">
                {{ newtitleData[item].status_map[statusKey] }}
                {{ test[item]?.name === '无人机流量' ? '架' : '个' }}
              </span>
            </div>
          </template>
@@ -190,6 +212,7 @@
const insureStatus = {
  0: '临近到期',
  1: '正常期限',
  2:'保险过期'
};
const moveListStatus = {
  '-1': '离线中',
@@ -199,13 +222,14 @@
const statusStyles = {
  机巢保险: {
    0: { class: 'expired', color: '#FFA600', background: '#FFA600' },
    1: { class: 'normal', color: 'rgba(27, 148, 255, 1)', background: 'rgba(27, 148, 255, 1)' },
    1: { class: 'normal', color: 'rgba(0, 180, 69, 1)', background: 'rgba(0, 180, 69, 1)' },
     2: { class: 'flying',color: 'rgba(255, 36, 36, 1)', background: 'rgba(255, 36, 36, 1)' },
  },
  无人机流量: {
    0: { class: 'offline', color: 'rgba(0, 180, 69, 1)', background: 'rgba(0, 180, 69, 1)' },
    1: { class: 'flying', color: 'rgba(186, 186, 186, 1)', background: 'rgba(186, 186, 186, 1)' },
    2: { class: 'flying', color: 'rgba(255, 36, 36, 1)', background: 'rgba(255, 36, 36, 1)' },
    1: { class: 'flying', color: 'rgba(255, 36, 36, 1)', background: 'rgba(255, 36, 36, 1)' },
    2: { class: 'flying', color: '#FFA600', background: '#FFA600' },
  },
  监控设备: {
    0: { class: 'offline', color: 'rgba(186, 186, 186, 1)', background: 'rgba(186, 186, 186, 1)' },
src/views/wel/components/taskOutcome.vue
New file
@@ -0,0 +1,314 @@
<template>
  <div class="taskOutcome">
    <div class="card-title">
      <div class="cardtotal">
        <p>任务成果</p>
        <span class="total-number">{{totalNum}}</span>
        <p>个</p>
        <img @click="refresh" src="/src/assets/images/workbench/st1.svg" alt="" />
      </div>
      <div class="time-card">
        <div
          class="card-item"
          :class="item === checked ? 'active' : ''"
          v-for="(item, index) in timeListEnum"
          :key="index"
          @click="timeClick(item, index)"
        >
          {{ timeListStr[index] }}
        </div>
      </div>
    </div>
    <div class="chart" ref="echartsRef"></div>
  </div>
</template>
<script setup>
import { getdisposeApi, gettaskOutcomApi } from '@/api/home/index';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import useEchartsResize from '@/hooks/useEchartsResize';
let checked = ref('CURRENT_YEAR');
let timeListStr = ['本周', '本月', '本年'];
let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR'];
const params = ref({
  dateEnum: 'CURRENT_YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
});
const totalNum = ref(0)
const dateSelect = ref('CURRENT_YEAR');
const refresh = () => {
  params.value.dateEnum = 'CURRENT_YEAR';
  checked.value = 'CURRENT_YEAR';
  dateSelect.value = 'CURRENT_YEAR';
  getBarChartData(params.value);
};
let timeClick = (item, index) => {
  checked.value = item;
  params.value.dateEnum = item;
  dateSelect.value = item;
 getBarChartData(params.value);
};
const seriesObj = {};
const echartsOption = {
    title: {
      text: seriesObj ? '' :'暂无数据',
      left: 'center',
      top: 'center',
      textStyle: {
        color: '#999',
        fontSize: '1.6rem',
        fontWeight: 'normal'
      }
    },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'shadow',
    },
  },
  legend: {
    itemWidth: 14,
    itemHeight: 8,
    data: ['照片总数', '视频总数', 'AI识别', '全景', '三维', '正射'], // 确保包含所有系列名称
    top: '-1%',
    textStyle: {
      color: '#383838',
      fontSize: 12,
    },
  },
  grid: {
    // top: '5%',
    left: 0,
    right: 0,
    bottom: 0,
    containLabel: true,
  },
  xAxis: {
    type: 'category',
    axisLine: {
      show: true, // 隐藏轴线
      lineStyle: {
        color: '#cdd5e2',
      },
    },
    axisLabel: {
      color: '#383838',
      // interval: 0, // 显示所有标签
    },
    data: [],
  },
  yAxis: [
    {
      type: 'value',
      name: '',
      nameTextStyle: {
        color: '#E6F7FF',
        fontSize: '1rem',
        padding: [-8, 0, 0, 0], // 调整单位文字位置,向左偏移
        verticalAlign: 'top',
        lineHeight: 12,
      },
      axisLabel: {
        interval: 0, // 显示所有标签
        color: '#383838',
        fontFamily: 'Source Han Sans CN, Source Han Sans CN',
        fontWeight: 400,
        fontSize: 10,
      },
      axisLine: {
        lineStyle: {
          color: '#383838',
        },
      },
      splitLine: {
        lineStyle: {
          color: '#cdd5e2',
          type: 'dashed', // 设置为虚线
        },
      },
    },
  ],
  series: [],
};
const eventTypeList = ref([
  {name: '照片总数', value: 0, type: '0', color: '#ED672D' },
  { name: '视频总数', value: 0, type: '1', color: '#F5B763' },
  { name: 'AI识别', value: 0, type: '2', color: '#6851C3' },
  {  name: '全景', value: 0, type: '5', color: '#FFE100' },
  {  name: '三维', value: 0, type: '3', color: '#04C598' },
  {  name: '正射', value: 0, type: '4', color: '#3673E8' },
]);
eventTypeList.value.forEach(item => {
  seriesObj[item.name] = {
    type: 'bar',
    stack: 'Ad',
    barWidth: 8,
    emphasis: {
      focus: 'series',
    },
    name: item.name,
    itemStyle: { color: item.color },
    data: [],
  };
});
const echartsRef = ref(null);
let { chart } = useEchartsResize(echartsRef);
// 获取柱状图数据
const getBarChartData = value => {
  gettaskOutcomApi(value).then(res => {
    const list = res?.data?.data || [];
    eventTypeList.value.forEach(item => {
      item.value = 0;
    });
    const typeMap = new Map();
    eventTypeList.value.forEach(item => {
      typeMap.set(item.type, item);
    });
    echartsOption.xAxis.data = list.map(item => item.name);
    Object.keys(seriesObj).forEach(key => {
      seriesObj[key].data = [];
    });
    // 遍历所有月份
    list.forEach(month => {
    totalNum.value = month.total
      const monthData = new Map();
      month.data.forEach(item => {
        monthData.set(item.name, item.value);
      });
      eventTypeList.value.forEach(typeItem => {
        const value = monthData.get(typeItem.type) || 0;
        if (seriesObj[typeItem.name]) {
          seriesObj[typeItem.name].data.push(value);
        }
      });
    });
    if (list.length > 0) {
      const lastMonth = list[list.length - 1];
      const lastMonthData = new Map();
      lastMonth.data.forEach(item => {
        lastMonthData.set(item.name, item.value);
      });
      eventTypeList.value.forEach(item => {
        const value = lastMonthData.get(item.type);
        if (value !== undefined) {
          item.value = value;
        }
      });
    }
    // 更新图表
    echartsOption.series = Object.values(seriesObj);
    chart.value.setOption(echartsOption);
  });
};
onMounted(() => {
  getBarChartData(params.value);
});
</script>
<style scoped lang="scss">
.taskOutcome {
  border-radius: 8px 8px 8px 8px;
  padding: 4px 14px 0 15px;
  background: #ffffff !important;
  margin-top: 10px;
  margin-left: 10px;
  height: pxToVh(400);
  .card-title {
    display: flex;
    margin-bottom: 10px;
    align-items: center;
    justify-content: space-between;
    .cardtotal {
      display: flex;
      align-items: center;
      margin-left: 9px;
      img {
        width: 17px;
        height: 17px;
        margin-left: 6px;
        cursor: pointer;
      }
      p {
        font-weight: bold;
        font-size: 14px;
        color: #363636;
      }
      span {
        font-weight: 400;
        font-size: 14px;
        color: #7c8091;
      }
      .total-number {
        font-family: 'Source Han Sans CN';
        font-weight: bold;
        font-size: 32px;
        color: #2a54ff;
      }
    }
    img {
      width: 36px;
      height: 40px;
    }
  }
}
.time-card {
  text-align: center;
  // height: 30px;
  height: pxToVh(30);
  background: #ffffff;
  border: 1px solid #e5e5e5;
  font-weight: 400;
  font-size: 14px;
  color: #7c8091;
  display: flex;
  width: 282px;
  margin-left: 15px;
  border-radius: 4px;
  .card-item {
    width: 94px;
    height: 100%;
    line-height: 28px;
    cursor: pointer;
    font-family: 'Source Han Sans CN';
    font-weight: 400;
    font-size: 14px;
    color: #7c8091;
  }
  .card-item:first-child {
    border-right: 1px solid #e5e5e5;
  }
  .card-item:nth-child(2) {
    border-right: 1px solid #e5e5e5;
  }
  .card-item.active {
    color: #1441ff;
    border: 1px solid #1c5cff;
  }
}
.chart {
  width: 98%;
  // height: 265px;
  height: pxToVh(285);
  padding-left: 10px;
  margin-top: 27px;
}
</style>
src/views/wel/index.vue
@@ -7,76 +7,20 @@
        <!-- 综合统计分析 -->
        <div class="comprehensiveCon">
          <div class="comprehensive">
            <div class="title">
              <div class="name">
                <span> 综合统计分析</span>
                <img @click="refresh" src="/src/assets/images/workbench/st1.png" alt="" />
                <div class="time-card">
                  <div class="card-item" :class="item === checked ? 'active' : ''" v-for="(item, index) in timeListEnum"
                    :key="index" @click="timeClick(item, index)">
                    {{ timeListStr[index] }}
                  </div>
                </div>
              </div>
              <div class="arrow" @click="jumpshebei">
                <img src="/src/assets/images/workbench/st2.png" alt="" />
              </div>
            </div>
            <div class="center">
              <div class="centerLeft">
                <!-- 工单统计 -->
                <div class="workOrder">
                  <div class="card-group">
                    <div class="main-card">
                      <div class="card-title">
                        <img :src="overviewImg1" alt="" />
                        <div class="cardtotal">
                          <p>工单统计占比</p>
                        </div>
                      </div>
                      <div class="status-grid">
                        <div class="status-item" v-for="(item, index) in eventTypeList" :key="index">
                          <div class="statusCon">
                            <div class="status-label">{{ item.name }}</div>
                            <div class="status-value">{{ item.value }}<span>个</span></div>
                            <div class="ratio">
                              占比
                              <span :style="{ color: item.color }">{{ ((item.rate * 100) / 100).toFixed(2) }}%</span>
                            </div>
                          </div>
                          <img :src="item.img" alt="" />
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="charts">
                    <div class="chart" ref="echartsRef"></div>
                  </div>
                </div>
                <proportionStatic></proportionStatic>
                <!-- 飞行统计 -->
                <div class="flyOrder">
                  <div class="fytitle">
                    <img :src="fy1" alt="" />
                    <span>飞行统计</span>
                  </div>
                  <div class="flycenter">
                    <div class="centerBox">
                      <div class="centerItem" v-for="(itemfly, index) in flyTypeList" :key="index">
                        <div class="flyimg"><img :src="itemfly.img" alt="" />{{ itemfly.name }}</div>
                        <div class="flydata">
                          <span>{{ itemfly.value }}</span>{{ unitMap[itemfly.name] }}
                        </div>
                      </div>
                    </div>
                    <div class="lineChart">
                      <div class="lineBox" ref="chartRef"></div>
                    </div>
                  </div>
                </div>
                <flightStatistics></flightStatistics>
              </div>
              <div class="centerRight">
                <flyratio :dateSelect="dateSelect"></flyratio>
                <!-- 机巢工单数量排名(件) -->
                <flyratio></flyratio>
                <!-- 任务成果 -->
  <taskOutcome></taskOutcome>
              </div>
            </div>
          </div>
@@ -92,6 +36,9 @@
</template>
<script setup>
import taskOutcome from './components/taskOutcome.vue'
import flightStatistics from './components/flightStatistics.vue'
import proportionStatic from './components/proportionStatic.vue'
import flyratio from './components/flyratio.vue'
import Bocklog from './components/backlog.vue'
import CalenBox from './components/calendarBox.vue'
@@ -100,14 +47,9 @@
import { mapGetters } from 'vuex'
import { getJobEventByStatus, getJobEventTotal, getFly, getFlyTime } from '@/api/home/index'
import overviewImg1 from '@/assets/images/workbench/tc1.png'
import overviewImg2 from '@/assets/images/workbench/tc2.svg'
import overviewImg3 from '@/assets/images/workbench/tc3.svg'
import overviewImg4 from '@/assets/images/workbench/tc4.svg'
import overviewImg5 from '@/assets/images/workbench/tc5.svg'
import fy1 from '@/assets/images/workbench/fy1.png'
import flyImg1 from '@/assets/images/workbench/fy2.svg'
import flyImg2 from '@/assets/images/workbench/fy3.svg'
import flyImg3 from '@/assets/images/workbench/fy4.svg'
import statistics from './components/statistics.vue'
import { ElMessage } from 'element-plus'
let checked = ref('CURRENT_YEAR')
@@ -124,377 +66,31 @@
  checked.value = item
  params.value.date_enum = item
  dateSelect.value = item
  getTypeData()
  getFlyList()
  getFlyTimeList()
}
const refresh = () => {
  params.value.date_enum = 'CURRENT_YEAR'
  checked.value = 'CURRENT_YEAR'
  dateSelect.value = 'CURRENT_YEAR'
  getTypeData()
  getFlyList()
  getFlyTimeList()
}
// 跳转
const jumpshebei = () => {
  ElMessage.warning('加急开发中...')
}
const eventTypeList = ref([
  { name: '待审核', value: 0, img: overviewImg2, color: '#FF472F', status: '2', rate: 0 },
  { name: '待处理', value: 0, img: overviewImg3, color: '#FF7411', status: '0', rate: 0 },
  { name: '处理中', value: 0, img: overviewImg4, color: '#FFC300', status: '3', rate: 0 },
  { name: '已完成', value: 0, img: overviewImg5, color: '#0291A1', status: '4', rate: 0 },
])
const keyMapping = {
  飞行时长: 'total_flight_time',
  飞行里程: 'total_flight_distance',
  任务成果: 'event_num',
}
const unitMap = {
  飞行时长: '时',
  飞行里程: '千米',
  任务成果: '个',
}
const flyTypeList = ref([
  { name: '飞行时长', value: 0, img: flyImg1 },
  { name: '飞行里程', value: 0, img: flyImg2 },
  { name: '任务成果', value: 0, img: flyImg3 },
])
const eventTotal = ref(0)
const data = ref([])
//  工单统计
const getTypeData = () => {
  getJobEventByStatus(params.value).then(res => {
    const resList = res?.data?.data || []
    resList.forEach(item => {
      eventTypeList.value.forEach(item1 => {
        if (item1.name === item.name) {
          item1.value = item.num
          item1.rate = item.rate
        }
      })
    })
    initChart(resList)
  })
}
// 飞行统计
const getFlyList = () => {
  getFly(params.value).then(res => {
    flyTypeList.value = flyTypeList.value.map(item => ({
      ...item,
      value: res.data.data[keyMapping[item.name]] || 0,
    }))
  })
}
const getFlyTimeList = () => {
  getFlyTime(params.value).then(res => {
    const resList = res?.data?.data || []
    lineCharts(resList)
  })
}
// 图表
const echartsRef = ref(null)
let { chart } = useEchartsResize(echartsRef)
const chartRef = ref(null)
let { chart: lineChart } = useEchartsResize(chartRef)
const initChart = val => {
  const totalNum = val.reduce((sum, item) => sum + item.num, 0)
  const data = {
    total: {
      title: '总计',
      figure: totalNum.toString(), // 动态计算总数
    },
    data: val.map(item => ({
      value: item.num,
      name: item.name,
      rate: item.rate,
    })),
  }
  const containerWidth = chart.value.clientWidth
  const isSmallScreen = containerWidth < 768 // 移动端判断
  const echartsOption = {
    color: ['#FF472F', '#FF7411', '#FFC300', '#0291A1'],
    tooltip: {
      trigger: 'item',
      padding: 0,
      borderWidth: 0,
      formatter: params => {
        return `<div style="background-color: rgba($color: #FFFFFF, $alpha: 0.95);
          box-shadow: 0 0.4rem 0.9rem 0 rgba($color: #000000, $alpha: 0.1);
          padding:0 1.2rem;
          display: flex;
          align-items: center;
          border-radius: 0.4rem;
          font-size: 1.2rem;" class="tooltip-area">
          <p>${params.marker}${params.name}</p>
          <h4 style="margin-left: 1rem;">${params.data.rate || params.percent}%</h4>
        </div>`
      },
    },
    legend: {
      show: false,
    },
    title: {
      text: data.total.title,
      textStyle: {
        color: 'rgba(28, 31, 35, 0.80)',
        fontSize: '1.2rem',
        fontWeight: 'bold',
      },
      subtext: data.total.figure,
      subtextStyle: {
        color: '#1C1F23',
        fontSize: '2rem',
        fontWeight: '600',
      },
      top: '40%',
      left: '48%',
      textAlign: 'center', // 文本对齐
    },
    series: [
      {
        name: '',
        type: 'pie',
        radius: ['43%', '63%'],
        avoidLabelOverlap: true,
        left: '-2%',
        label: {
          formatter: params => {
            // 使用 b 样式标记包裹百分比数值
            return `{a|${params.name}}: {b|${params.data.rate}%}`
          },
          fontSize: isSmallScreen ? '0.8rem' : '1.2rem',
          position: isSmallScreen ? 'outer' : 'outer',
          alignTo: 'labelLine',
          overflow: 'truncate',
          overflow: 'none', // 禁用省略号
          bleedMargin: 30, // 防止标签被截断
          rich: {
            a: {
              color: 'rgba(28, 31, 35, 0.80)',
              fontSize: isSmallScreen ? '1rem' : '1.2rem',
            },
            b: {
              color: '#1C1F23',
              fontSize: isSmallScreen ? '1rem' : '1.3rem',
              fontWeight: 'bold',
            },
          },
        },
        labelLine: {
          show: true,
          length: 3, // 调整引导线长度
          length2: 5,
          lineStyle: {
            cap: 'round',
          },
          minTurnAngle: 45, // 防止小角度重叠
        },
        data: data.data,
      },
    ],
  }
  chart.value.setOption(echartsOption)
}
// 折线/柱状图
const lineCharts = bardata => {
  const categories = bardata?.map(item => item.name) // x轴类别
  const flight_distance = []
  const flight_time = []
  const event_num = []
  // /遍历数据填充各系列
  bardata?.forEach(period => {
    let hasDuration = false,
      hasDistance = false,
      hasResult = false
    period.data?.forEach(item => {
      switch (item.name) {
        case '飞行时长':
          flight_time.push(parseFloat(item.value) || 0)
          hasDuration = true
          break
        case '飞行里程':
          // 转换为万公 (假设原始单位是米)
          // flight_distance.push((parseFloat(item.value) / 10 || 0));
          flight_distance.push(parseFloat(item.value) || 0)
          hasDistance = true
          break
        case '任务成果':
          event_num.push(Number(item.value) || 0)
          hasResult = true
          break
      }
    })
    // 处理可能缺失的数据项
    if (!hasDuration) flight_time.push(0)
    if (!hasDistance) flight_distance.push(0)
    if (!hasResult) event_num.push(0)
  })
  var option = {
    tooltip: {
      trigger: 'item',
      axisPointer: {
        type: 'shadow',
      },
    },
    grid: {
      left: '2%',
      right: '4%',
      bottom: '14%',
      top: '16%',
      containLabel: true,
    },
    legend: {
      data: ['飞行时长', '飞行里程', '任务成果'],
      left: 'center',
      top: '5%',
      color: '#383838', // 直接配置颜色
      itemWidth: 15,
      itemHeight: 10,
      itemGap: 25,
      fontSize: '1.2rem'
    },
    xAxis: {
      type: 'category',
      data: categories,
      axisLine: {
        lineStyle: {
          color: '#cdd5e2',
        },
      },
      axisLabel: {
        interval: 0,
        color: '#383838',
      },
    },
    yAxis: [
      {
        type: 'log',
        name: '单位:万',
        nameTextStyle: {
          color: '#383838',
          fontSize: '1.2rem'
        },
        min: 1,
        logBase: 3,
        axisLine: {
          show: false,
          lineStyle: {
            color: '#cdd5e2',
          },
        },
        splitLine: {
          show: true,
          lineStyle: {
            type: 'dashed',
            color: '#cdd5e2',
            width: 1,
            opacity: 0.5,
          },
        },
        axisLabel: {
          color: '#666666', // 直接配置颜色
        },
      },
      {
        type: 'log',
        name: '',
        nameTextStyle: {
          color: '#383838',
        },
        min: 1,
        logBase: 3,
        axisLine: {
          show: false,
          lineStyle: {
            color: '#cdd5e2',
          },
        },
        splitLine: {
          show: true,
          lineStyle: {
            type: 'dashed',
            color: '#cdd5e2',
            width: 1,
            opacity: 0.5,
          },
        },
        axisLabel: {
          color: '#383838', // 直接配置颜色
        },
      },
    ],
    series: [
      {
        name: '飞行时长',
        type: 'bar',
        barWidth: '8px',
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: '#29acff' },
            { offset: 1, color: '#4bdfff' },
          ]),
          borderRadius: 6,
        },
        data: flight_time,
      },
      {
        name: '飞行里程',
        type: 'bar',
        barWidth: '8px',
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: '#01c871' },
            { offset: 1, color: '#55f49c' },
          ]),
          borderRadius: 6,
        },
        data: flight_distance,
      },
      {
        name: '任务成果',
        type: 'line',
        yAxisIndex: 1,
        smooth: true,
        // itemStyle: {
        //   color: '#ffa43a', // 默认状态颜色
        //   borderColor: 'rgba(255, 234, 0, 0.5)',
        //   borderWidth: 5,
        // },
        // emphasis: {
        //   // 悬停状态
        //   itemStyle: {
        //     color: '#ff8c00', // 悬停时颜色变深
        //     borderWidth: 6,
        //   },
        // },
        lineStyle: {
          color: 'rgba(52, 146, 242, 1)',
        },
        data: event_num,
      },
    ],
  }
  lineChart.value.setOption(option)
}
onMounted(() => {
  getJobEventTotal().then(res => {
    eventTotal.value = res?.data?.data || 0
  })
  getTypeData()
  lineCharts()
  getFlyList()
  getFlyTimeList()
})
</script>
@@ -518,13 +114,13 @@
  height: 100%;
  .comprehensiveCon {
    background: #ffffff !important;
    // background: #ffffff !important;
    height: pxToVh(776);
    border-radius: 8px 8px 8px 8px;
    .comprehensive {
      padding: 14px 14px 0 21px;
      // padding: 14px 14px 0 21px;
      .title {
        display: flex;
@@ -595,220 +191,14 @@
      // 工、单
      .center {
        display: flex;
        .centerLeft {
          width: 60%;
          border-right: 1px solid #dfdfdf;
          width: 50%;
          .workOrder {
            margin-top: 11px;
            display: flex;
            justify-content: space-between;
            border-bottom: 1px solid #dfdfdf;
            .card-group {
              width: 40%;
              .card-title {
                display: flex;
                margin-bottom: 10px;
                align-items: center;
                img {
                  width: 36px;
                  height: 40px;
                }
              }
              .cardtotal {
                display: flex;
                align-items: center;
                margin-left: 9px;
                p {
                  font-weight: bold;
                  font-size: 14px;
                  color: #363636;
                }
                span {
                  font-weight: 400;
                  font-size: 14px;
                  color: #7c8091;
                }
                .total-number {
                  font-family: 'Source Han Sans CN';
                  font-weight: bold;
                  font-size: 32px;
                  color: #2a54ff;
                }
              }
              .status-grid {
                display: grid;
                grid-template-columns: repeat(2, 1fr);
                row-gap: 14px;
                gap: 10px;
                padding-bottom: 5px;
                .status-item {
                  display: flex;
                  text-align: center;
                  justify-content: space-between;
                  height: 97px;
                  // height: pxToVh(107);
                  max-width: 158px;
                  background: #f6f8fe;
                  border-radius: 8px 8px 8px 8px;
                  img {
                    width: 26px;
                    height: 26px;
                    padding: 9px 10px 9px 2px;
                  }
                  .statusCon {
                    display: flex;
                    flex-direction: column;
                    // align-items: center;
                    padding: 5px 4px 9px 10px;
                    text-align: left;
                    .status-label {
                      font-weight: 400;
                      font-size: 14px;
                      text-align: left;
                      color: #383838;
                    }
                    .ratio {
                      font-weight: 400;
                      font-size: 12px;
                      color: #363636;
                      white-space: nowrap;
                      text-align: left;
                    }
                    .status-value {
                      font-family: 'Source Han Sans CN';
                      font-weight: bold;
                      font-size: 30px;
                      color: #363636;
                      // margin: 5px 0;
                      // font-style: italic;
                      display: inline-block;
                      transform: skewX(-5deg);
                      span {
                        font-weight: 400;
                        font-size: 14px;
                        color: #7c8091;
                      }
                    }
                  }
                }
              }
            }
            .charts {
              margin-top: 30px;
              width: 100%;
              height: auto;
              .chart {
                width: 100%;
                height: 100%;
              }
            }
          }
          // 飞行统计
          .flyOrder {
            margin-top: 5px;
            .fytitle {
              display: flex;
              align-items: center;
              img {
                width: 39px;
                height: 38px;
              }
              span {
                font-weight: bold;
                font-size: 14px;
                color: #363636;
              }
            }
            .flycenter {
              margin-top: 13px;
              .centerBox {
                display: flex;
                justify-content: space-between;
                margin-bottom: 10px;
                .centerItem {
                  padding: 7px 7px 0 15px;
                  // width: 196px;
                  flex: 1;
                  // height: 80px;
                  height: pxToVh(80);
                  background: #f6f8fe;
                  box-shadow: 0px 5px 4px 0px #ebf1ff;
                  border-radius: 8px 8px 8px 8px;
                  border: 1px solid #ffffff;
                  margin-right: 16px;
                  img {
                    width: 15px;
                    height: 15px;
                    margin-right: 5px;
                  }
                  .flyimg {
                    display: flex;
                    align-items: center;
                    font-weight: 400;
                    font-size: 14px;
                    color: #7c8091;
                  }
                  .flydata {
                    margin-top: 10px;
                    text-align: right;
                    font-size: 14px;
                    color: #7c8091;
                    span {
                      font-weight: bold;
                      font-size: 24px;
                      color: #343434;
                      margin-right: 6px;
                    }
                  }
                }
              }
              .lineChart {
                height: 209px;
                width: 100%;
                .lineBox {
                  width: 100%;
                  height: 100%;
                }
              }
            }
          }
        }
        .centerRight {
          width: 40%;
          width: 50%;
        }
      }
    }