无人机管理后台前端(已迁走)
张含笑
2025-05-09 b6aad0df3b7226fd5ad2ddeccd0851d72dc7fda4
个人工作台处理
41 files added
1614 ■■■■■ changed files
public/fonts/SourceHanSansCN-Bold.otf patch | view | raw | blame | history
public/fonts/SourceHanSansCN-Medium.otf patch | view | raw | blame | history
public/fonts/SourceHanSansCN-Regular.otf patch | view | raw | blame | history
public/fonts/YouSheBiaoTiHei.TTF patch | view | raw | blame | history
public/fonts/font.css 53 ●●●●● patch | view | raw | blame | history
public/img/bg/index_logo2.png patch | view | raw | blame | history
src/api/algorithm.js 19 ●●●●● patch | view | raw | blame | history
src/api/home/index.js 50 ●●●●● patch | view | raw | blame | history
src/assets/images/ht-sfbg-click.png patch | view | raw | blame | history
src/assets/images/ht-sfbg-hover.png patch | view | raw | blame | history
src/assets/images/ht-sfbg.png patch | view | raw | blame | history
src/assets/images/workbench/fy1.png patch | view | raw | blame | history
src/assets/images/workbench/fy2.png patch | view | raw | blame | history
src/assets/images/workbench/fy3.png patch | view | raw | blame | history
src/assets/images/workbench/fy4.png patch | view | raw | blame | history
src/assets/images/workbench/jc1.png patch | view | raw | blame | history
src/assets/images/workbench/jc2.png patch | view | raw | blame | history
src/assets/images/workbench/st1.png patch | view | raw | blame | history
src/assets/images/workbench/st2.png patch | view | raw | blame | history
src/assets/images/workbench/st3.png patch | view | raw | blame | history
src/assets/images/workbench/st4.png patch | view | raw | blame | history
src/assets/images/workbench/st5.png patch | view | raw | blame | history
src/assets/images/workbench/st6.png patch | view | raw | blame | history
src/assets/images/workbench/st7.png patch | view | raw | blame | history
src/assets/images/workbench/st8.png patch | view | raw | blame | history
src/assets/images/workbench/st9.png patch | view | raw | blame | history
src/assets/images/workbench/tc1.png patch | view | raw | blame | history
src/assets/images/workbench/tc2.png patch | view | raw | blame | history
src/assets/images/workbench/tc3.png patch | view | raw | blame | history
src/assets/images/workbench/tc4.png patch | view | raw | blame | history
src/assets/images/workbench/tc5.png patch | view | raw | blame | history
src/assets/images/workbench/tc6.png patch | view | raw | blame | history
src/assets/map_images/EndPointicon.png patch | view | raw | blame | history
src/assets/map_images/Startingpointicon.png patch | view | raw | blame | history
src/hooks/useEchartsResize.js 38 ●●●●● patch | view | raw | blame | history
src/utils/coordinateTransformation.js 103 ●●●●● patch | view | raw | blame | history
src/views/algorithmRepository/algorithmRepository.vue 336 ●●●●● patch | view | raw | blame | history
src/views/wel/components/backlog.vue 212 ●●●●● patch | view | raw | blame | history
src/views/wel/components/calendarBox.vue 158 ●●●●● patch | view | raw | blame | history
src/views/wel/components/flyratio.vue 198 ●●●●● patch | view | raw | blame | history
src/views/wel/components/statistics.vue 447 ●●●●● patch | view | raw | blame | history
public/fonts/SourceHanSansCN-Bold.otf
Binary files differ
public/fonts/SourceHanSansCN-Medium.otf
Binary files differ
public/fonts/SourceHanSansCN-Regular.otf
Binary files differ
public/fonts/YouSheBiaoTiHei.TTF
Binary files differ
public/fonts/font.css
New file
@@ -0,0 +1,53 @@
@font-face {
  font-family: "Source Han Sans CN";
  src: url("SourceHanSansCN-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
}
@font-face {
  font-family: "Source Han Sans CN";
  src: url("SourceHanSansCN-Medium.otf") format("opentype");
  font-weight: 500;
  font-style: normal;
}
@font-face {
  font-family: "Source Han Sans CN";
  src: url("SourceHanSansCN-Bold.otf") format("opentype") ;
  font-weight: 700;
  font-style: normal;
}
@font-face {
  font-family: "Segoe UI";
  src: url("SourceHanSansCN-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
}
@font-face {
  font-family: "Segoe UI";
  src: url("SourceHanSansCN-Medium.otf") format("opentype");
  font-weight: 500;
  font-style: normal;
}
@font-face {
  font-family: "Segoe UI";
  src: url("SourceHanSansCN-Bold.otf") format("opentype") ;
  font-weight: 700;
  font-style: normal;
}
/* 额外的标题字体 */
@font-face {
  font-family: "YouSheBiaoTiHei";
  src: url("YouSheBiaoTiHei.TTF") format("truetype");
  font-weight: normal;
  font-style: normal;
}
public/img/bg/index_logo2.png
src/api/algorithm.js
New file
@@ -0,0 +1,19 @@
// 算法仓库
import request from '@/axios'
// 图片列表
export const getalgorithmList = (data, params) => {
    return request({
      url: `/drone-device-core/aiTmp/list`,
      method: 'post',
      data,
      params
    })
  }
//机巢查询
export const selectDeviceList = data => {
    return request({
        url: `/drone-device-core/aiTmp/devices`,
        method: 'get',
        data: data,
    })
}
src/api/home/index.js
New file
@@ -0,0 +1,50 @@
import request from '@/axios'
// 事件概况总数
export const getJobEventTotal = () => {
    return request({
        url: '/drone-device-core/jobEvent/total',
        method: 'get',
    })
}
// 事件概况分类数
export const getJobEventByStatus = data => {
    return request({
        url: '/drone-device-core/jobEvent/eventByStatus',
        method: 'post',
        data,
    })
}
// 行业任务统计
export const industryJobNumPieChart = data => {
    return request({
        url: '/drone-device-core/jobEvent/deviceEventList',
        method: 'post',
        data: data,
    })
}
// 事件任务统计
export const jobEventBar = data => {
    return request({
        url: '/drone-device-core/wayline/waylineJobInfo/jobEventBar',
        method: 'post',
        data: data,
    })
}
// 设备统计
export const getStatics = (areaCode) => {
    return request({
      url: '/drone-device-core/manage/api/v1/devices/deviceStatistics',
      method: 'post',
      params: {
        areaCode
      },
    });
  };
  // 日历
export const getCalen = data => {
    return request({
        url: '/drone-device-core/wayline/waylineJobInfo/jobEventBar',
        method: 'post',
        data,
    })
}
src/assets/images/ht-sfbg-click.png
src/assets/images/ht-sfbg-hover.png
src/assets/images/ht-sfbg.png
src/assets/images/workbench/fy1.png
src/assets/images/workbench/fy2.png
src/assets/images/workbench/fy3.png
src/assets/images/workbench/fy4.png
src/assets/images/workbench/jc1.png
src/assets/images/workbench/jc2.png
src/assets/images/workbench/st1.png
src/assets/images/workbench/st2.png
src/assets/images/workbench/st3.png
src/assets/images/workbench/st4.png
src/assets/images/workbench/st5.png
src/assets/images/workbench/st6.png
src/assets/images/workbench/st7.png
src/assets/images/workbench/st8.png
src/assets/images/workbench/st9.png
src/assets/images/workbench/tc1.png
src/assets/images/workbench/tc2.png
src/assets/images/workbench/tc3.png
src/assets/images/workbench/tc4.png
src/assets/images/workbench/tc5.png
src/assets/images/workbench/tc6.png
src/assets/map_images/EndPointicon.png
src/assets/map_images/Startingpointicon.png
src/hooks/useEchartsResize.js
New file
@@ -0,0 +1,38 @@
import * as echarts from 'echarts'
/**
 * ECharts自适应容器大小Hook
 *
 * @param {Object} domRef - DOM元素引用对象,需包含value属性指向要挂载的DOM节点
 * @returns {Object} chart - 包含value属性的对象,value保存ECharts实例引用
 */
const useEchartsResize = (domRef) => {
    // 图表实例容器,用对象包装便于保持引用地址
    const chart = {value: null}
    // 窗口大小变化处理函数
    const resizeChart = () => {
        chart.value?.resize()
    }
    // 组件挂载后初始化逻辑
    onMounted(() => {
        if (!domRef.value) return
        // 初始化ECharts实例并注册resize监听
        chart.value = echarts.init(domRef.value)
        window.addEventListener('resize', resizeChart)
    })
    // 组件卸载前清理逻辑
    onBeforeUnmount(() => {
        // 移除监听并清理图表实例
        window.removeEventListener('resize', resizeChart)
        chart.value?.dispose()
        chart.value = null
    })
    return {chart}
}
export default useEchartsResize
src/utils/coordinateTransformation.js
New file
@@ -0,0 +1,103 @@
/*
 * @Author: GuLiMmo 2820890765@qq.com
 * @Date: 2024-06-12 13:51:57
 * @LastEditors: GuLiMmo 2820890765@qq.com
 * @LastEditTime: 2024-06-12 13:54:00
 * @FilePath: /bigScreen/src/utils/coordinateTransformation.js
 * @Description:
 * Copyright (c) 2024 by GuLiMmo, All Rights Reserved.
 */
const PI = Math.PI;
const a = 6378245.0;
const ee = 0.00669342162296594323;
export function wgs84ToGcj02(lng, lat) {
    if (out_of_china(lng, lat)) {
        return [lng, lat];
    } else {
        var dlat = transformlat(lng - 105.0, lat - 35.0);
        var dlng = transformlng(lng - 105.0, lat - 35.0);
        var radlat = (lat / 180.0) * PI;
        var magic = Math.sin(radlat);
        magic = 1 - ee * magic * magic;
        var sqrtmagic = Math.sqrt(magic);
        dlat = (dlat * 180.0) / (((a * (1 - ee)) / (magic * sqrtmagic)) * PI);
        dlng = (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI);
        var mglat = lat + dlat;
        var mglng = lng + dlng;
        return [mglng, mglat];
    }
}
export function gcj02ToWgs84(lng, lat) {
    if (out_of_china(lng, lat)) {
        return [lng, lat];
    } else {
        var dlat = transformlat(lng - 105.0, lat - 35.0);
        var dlng = transformlng(lng - 105.0, lat - 35.0);
        var radlat = (lat / 180.0) * PI;
        var magic = Math.sin(radlat);
        magic = 1 - ee * magic * magic;
        var sqrtmagic = Math.sqrt(magic);
        dlat = (dlat * 180.0) / (((a * (1 - ee)) / (magic * sqrtmagic)) * PI);
        dlng = (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI);
        var mglat = lat + dlat;
        var mglng = lng + dlng;
        return [lng * 2 - mglng, lat * 2 - mglat];
    }
}
function transformlat(lng, lat) {
    var ret =
        -100.0 +
        2.0 * lng +
        3.0 * lat +
        0.2 * lat * lat +
        0.1 * lng * lat +
        0.2 * Math.sqrt(Math.abs(lng));
    ret +=
        ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) *
            2.0) /
        3.0;
    ret +=
        ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) *
            2.0) /
        3.0;
    ret +=
        ((160.0 * Math.sin((lat / 12.0) * PI) +
            320 * Math.sin((lat * PI) / 30.0)) *
            2.0) /
        3.0;
    return ret;
}
function transformlng(lng, lat) {
    var ret =
        300.0 +
        lng +
        2.0 * lat +
        0.1 * lng * lng +
        0.1 * lng * lat +
        0.1 * Math.sqrt(Math.abs(lng));
    ret +=
        ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) *
            2.0) /
        3.0;
    ret +=
        ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) *
            2.0) /
        3.0;
    ret +=
        ((150.0 * Math.sin((lng / 12.0) * PI) +
            300.0 * Math.sin((lng / 30.0) * PI)) *
            2.0) /
        3.0;
    return ret;
}
// 判断是否在国内,不在国内则不做偏移
function out_of_china(lng, lat) {
    return (
        lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271 || false
    );
}
src/views/algorithmRepository/algorithmRepository.vue
New file
@@ -0,0 +1,336 @@
<template>
  <basic-container>
    <div class="algorithContainer">
      <div class="algorithItem" v-if="!showDetail">
        <div
          class="item"
          v-for="(item, index) in AlgorithmData"
          :key="index"
          :class="{ 'active-bg': activeItem === item.dictValue }"
          @click="jumpDatail(item)"
        >
          <img class="imgicon" :src="`${baseUrl}/后台-算法仓库/${item.dictValue}.png`" alt="" />
          <div>{{ item.dictValue }}</div>
          <div
            :class="!statusSign && item.dictValue === nameSign ? 'stopStatus' : 'normalStatus'"
            @click.stop="changeStatus(item)"
          >
            {{ !statusSign && item.dictValue === nameSign ? '停用状态' : '正常状态' }}
          </div>
        </div>
      </div>
      <!-- 详情页 -->
      <div class="algorithItemDetail" v-else>
        <div class="search">
          <div class="searchBox">
            <div class="item">
              <div class="itemchild">模糊查询:</div>
              <el-input
                v-model="params.name"
                class="filter-item"
                placeholder="请输入事件名称"
                clearable
              ></el-input>
            </div>
            <div class="item">
              <el-date-picker
                type="daterange"
                range-separator="至"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
                value-format="YYYY-MM-DD"
                v-model="taskData"
                placeholder="请选择日期"
                @change="changeselect"
              />
            </div>
            <div class="item">
              <div class="itemchild">机巢查询:</div>
              <el-select v-model="params.device_name" placeholder="请选择" class="filter-item">
                <el-option v-for="item in jcoptions" :key="item" :label="item" :value="item" />
              </el-select>
            </div>
          </div>
          <div class="search-btn">
            <el-button type="primary" icon="el-icon-back" @click="goback">返回</el-button>
            <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
            <el-button icon="el-icon-delete" @click="handleReset">清空</el-button>
          </div>
        </div>
        <div
          class="pictureitem"
          v-if="detailData.length > 0"
          v-loading="loading"
          element-loading-text="加载中"
        >
          <div class="imgitem" v-for="(item, index) in detailData" :key="index">
            <img :src="item.url" alt="" />
            <div class="info">
              <div class="name">{{ item.name }}</div>
              <div class="time">{{ item.create_time.slice(5, 16).replace('-', '/', 1) }}</div>
            </div>
          </div>
        </div>
        <el-empty class="custom-empty" v-else>
          <template #description>
            <span class="custom-text">暂无数据</span>
          </template>
        </el-empty>
        <!-- 分页 -->
        <el-pagination
          class="pageStyle"
          background
          :page-sizes="[10, 20, 30, 50]"
          v-model:current-page="params.current"
          v-model:page-size="params.size"
          layout="total, prev, pager, next,sizes, jumper"
          :total="total"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </div>
  </basic-container>
</template>
<script setup>
defineOptions({
  name: 'algorithmRepository',
});
import { getDictionaryByCode } from '@/api/system/dictbiz';
import { getalgorithmList, selectDeviceList } from '@/api/algorithm';
import { useRouter } from 'vue-router';
const router = useRouter();
const baseUrl = import.meta.env.VITE_APP_TERRAIN_URL;
const showDetail = ref(false);
const taskData = ref('');
const jcvalue = ref('');
const jcoptions = ref([]);
const total = ref(0);
const loading = ref(true);
const params = ref({
  ai_type_key: '',
  start_date: null,
  end_date: null,
  device_name: '',
  name: '',
  current: 1,
  size: 10,
});
// 请求字典字段
let AlgorithmData = ref([]);
const detailData = ref([]);
const requestDictionary = () => {
  getDictionaryByCode('SF').then(res => {
    if (res.code !== 0) {
      // 处理数据
      AlgorithmData.value = res.data.data['SF'];
    }
  });
};
const activeItem = ref(null);
const jumpDatail = val => {
  showDetail.value = true;
  activeItem.value = val.dictValue;
  params.value.ai_type_key = val.dictKey;
  getList();
};
const statusSign = ref(true);
const nameSign = ref('');
const changeStatus = val => {
  nameSign.value = val.dictValue;
  statusSign.value = !statusSign.value;
};
// 详情
const getList = () => {
  getalgorithmList(params.value).then(res => {
    loading.value = true;
    detailData.value = res.data.data.records;
    total.value = res.data.data.total;
    setTimeout(() => {
      loading.value = false;
    }, 1000);
  });
};
// 机巢查询
const getDeviceList = () => {
  selectDeviceList().then(res => {
    jcoptions.value = res.data.data;
  });
};
// 日期选择
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;
};
const handleSearch = () => {
  getList();
};
const handleReset = () => {
  params.value.start_date = null;
  params.value.end_date = null;
  params.value.device_name = '';
  params.value.name = '';
  getList();
};
const goback = () => {
  showDetail.value = false;
  activeItem.value = null;
};
// 分页大小改变
const handleSizeChange = val => {
  params.size = val;
  getList();
};
// 页码改变
const handleCurrentChange = val => {
  params.current = val;
  getList();
};
onMounted(() => {
  requestDictionary();
  getDeviceList();
});
</script>
<style scoped lang="scss">
.algorithItem {
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 53px;
  text-align: center;
  vertical-align: middle;
  .item {
    height: 300px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background: url('/src/assets/images/ht-sfbg.png') no-repeat center;
    background-size: 100% 100%;
    .imgicon {
      width: 100px;
      height: 100px;
      margin-bottom: 47px;
    }
    &:hover {
      background: url('/src/assets/images/ht-sfbg-hover.png') no-repeat center;
      background-size: 100% 100%;
    }
    &.active-bg {
      background: url('/src/assets/images/ht-sfbg-click.png') no-repeat center;
      background-size: 100% 100%;
    }
    .normalStatus {
      width: 116px;
      height: 43px;
      background: #ebfbee;
      border-radius: 50px;
      text-align: center;
      line-height: 43px;
      font-weight: 400;
      font-size: 16px;
      color: #029d36;
      margin-top: 23px;
      cursor: pointer;
    }
    .stopStatus {
      width: 116px;
      height: 43px;
      background: #e8e8e8;
      border-radius: 50px 50px 50px 50px;
      text-align: center;
      line-height: 43px;
      font-weight: 400;
      font-size: 16px;
      color: #464747;
      margin-top: 23px;
      cursor: pointer;
    }
    .normalStatus:hover {
      background: rgba(6, 217, 87, 0.2);
    }
  }
}
.algorithItemDetail {
  padding: 20px;
  .pictureitem {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 24px;
    .imgitem {
      border-radius: 12px 12px 0 0;
      overflow: hidden;
      img {
        width: 100%;
        height: 200px;
        display: block;
        margin: 0;
        padding: 0;
      }
      .info {
        display: flex;
        justify-content: space-between;
        align-items: center;
        height: 42px;
        border-radius: 0 0 12px 12px;
        overflow: hidden;
        background: linear-gradient(180deg, #ffffff 0%, #e5edff 100%);
        border: 1px solid #1c5cff;
        border-top: none !important;
        .name {
          margin-left: 12px;
          font-weight: 500;
          font-size: 16px;
          color: #363636;
        }
        .time {
          margin-right: 12px;
          font-weight: 500;
          font-size: 14px;
          color: #595959;
        }
      }
    }
  }
  .search {
    display: flex;
    justify-content: space-between;
  }
  .searchBox {
    display: flex;
    align-items: center;
    margin-bottom: 27px;
    .itemchild {
      white-space: nowrap;
      margin-right: 5px;
      font-weight: 400;
      font-size: 14px;
      color: #363636;
    }
    .item {
      display: flex;
      align-items: center;
      margin-right: 49px;
    }
  }
  .search-btn {
    display: flex;
  }
  .filter-item {
    width: 218px;
  }
}
.pageStyle {
  margin-top: 32px;
  display: flex;
  justify-content: right;
}
</style>
src/views/wel/components/backlog.vue
New file
@@ -0,0 +1,212 @@
<template>
  <div class="bocklogBox">
    <div class="block">
      <div class="title">待办事项</div>
      <div class="todo-items">
        <div
          v-for="(item, index) in todos"
          :key="index"
          class="todo-item"
          :class="`status-${item.status}`"
        >
          <div class="status-indicator"></div>
          <div class="content-wrapper">
            <div class="main-content">
              <span class="status-tag">{{ statusMap[item.status] }}</span>
              <span class="todo-text">{{ item.title }}</span>
            </div>
            <div class="action-area">
              <img :src="st7" alt="">
              <span class="todo-date">{{ item.date }}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import st7 from '@/assets/images/workbench/st7.png'
// 状态显示映射
const statusMap = {
  pending: '待审核',
  processing: '处理中',
  todo: '待处理',
};
// 待办事项数据
const todos = ref([
  {
    status: 'pending',
    title: '发现暴露垃圾事件',
    date: '2025.03.26',
  },
  {
    status: 'todo',
    title: '发现暴露垃圾事件',
    date: '2025.03.26',
  },
  {
    status: 'processing',
    title: '发现暴露垃圾事件',
    date: '2025.03.26',
  },
  {
    status: 'pending',
    title: '发现暴露垃圾事件',
    date: '2025.03.26',
  },
  {
    status: 'todo',
    title: '发现暴露垃圾事件',
    date: '2025.03.26',
  },
]);
</script>
<style lang="scss" scoped>
.bocklogBox {
  width: 100%;
  height: 306px;
  background: rgba(255, 255, 255, 0.41);
  box-shadow: 0px 3px 4px -1px rgba(125, 125, 125, 0.25);
  border-radius: 8px 8px 8px 8px;
  border: 2px solid #ffffff;
  .block {
    margin: 11px 21px 13px 22px;
    .title {
      font-weight: bold;
      font-size: 16px;
      color: #363636;
    }
    .todo-items {
      display: grid;
      gap: 0.8rem;
      margin-top: 10px;
    }
    .todo-item {
      display: flex;
      align-items: center;
     background: #FFFFFF;
      border-radius: 8px;
      box-shadow: 0px 2px 4px 0px rgba(173, 173, 173, 0.18);
      transition: all 0.2s ease;
      border-left: 4px solid transparent;
      height: 42px;
      &:hover {
        transform: translateY(-2px);
        box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
      }
      .status-indicator {
        margin-left: 18px;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        flex-shrink: 0;
      }
      .content-wrapper {
        flex: 1;
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
      }
      .main-content {
        display: flex;
        align-items: center;
        gap: 0.8rem;
        .status-tag {
          font-weight: 400;
          font-size: 14px;
          padding: 0.3rem 0.5rem;
          border-radius: 12px;
        }
        .todo-text {
          font-size: 14px;
          color: #343434;
        }
      }
      .action-area {
        display: flex;
        align-items: center;
     margin-right: 42px;
        .todo-date {
          color: #666;
          font-size: 0.9rem;
        }
      }
    }
    // 状态颜色方案
    .status-pending {
      border-left-color: #FF6560;
      color: #FF6560;
      .status-indicator {
        background: #FF6560;
      }
    }
    .status-todo {
      border-left-color: #5D77FB;
      color: #5D77FB;
      .status-indicator {
        background: #5D77FB;
      }
    }
    .status-processing {
      border-left-color: #FF8B26;
      color: #FF8B26;
      .status-indicator {
        background: #FF8B26;
      }
    }
  }
  //   @media (max-width: 768px) {
  //     .todo-list-container {
  //       padding: 1rem;
  //       .content-wrapper {
  //         flex-direction: column;
  //         align-items: flex-start !important;
  //         gap: 0.5rem !important;
  //       }
  //       .action-area {
  //         width: 100%;
  //         justify-content: space-between;
  //       }
  //     }
  //   }
  //   @media (max-width: 480px) {
  //     .main-content {
  //       flex-direction: column;
  //       align-items: flex-start !important;
  //     }
  //     .status-tag {
  //       margin-bottom: 0.3rem;
  //     }
  //   }
}
</style>
src/views/wel/components/calendarBox.vue
New file
@@ -0,0 +1,158 @@
<template>
  <div class="calenBox">
    <el-calendar ref="calendar" v-model="leftValue" @change="handleMonthChange(date)">
      <template #date-cell="{ data }">
        <div>
          <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"
            >
              <span></span>
              {{ event.content }}
            </div>
          </div>
        </div>
      </template>
    </el-calendar>
  </div>
</template>
<script setup>
import dayjs from 'dayjs';
import { jobEventBar, getCalen } from '@/api/home/index';
// 日历事件数据
// 格式: 'YYYY-MM-DD': [...events]
const events = ref({
  '2025-05-01': [
    { type: 'work-order', content: '工单26' },
    { type: 'task', content: '任务26' },
  ],
  '2025-05-04': [
    { type: 'task', content: '任务26' },
    { type: 'work-order', content: '工单26' },
  ],
});
const params = ref({
  end_date: undefined,
  start_date: undefined,
});
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());
watch(
  () => leftValue.value,
  (newV, oldV) => {
    if (newV && dayjs(newV).isSame(dayjs(), 'day')) {
      console.log('点击了今天');
    }
    if (newV && oldV) {
       const newDate = dayjs(newV);
      const oldDate = dayjs(oldV);
      // 格式化为 YYYY-MM-DD HH:mm:ss
      const newDateStr = newDate.format('YYYY-MM-DD HH:mm:ss');
      const oldDateStr = oldDate.format('YYYY-MM-DD HH:mm:ss');
      params.
      console.log('新日期:', newDateStr, '旧日期:', oldDateStr);
      if (newDate.isBefore(oldDate, 'month')) {
        console.log('点击了上个月',newDateStr);
      } else if (newDate.isAfter(oldDate, 'month')) {
        console.log('点击了下个月',newDateStr);
      }
    }
  },
  { deep: true }
);
const handleMonthChange = date => {
  console.log('date', date);
  // 获取当前视图月份的第一天
  const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
  // 获取当前视图月份的最后一天
  const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
  // 格式化为YYYY-MM-DD格式(可选)
  const formatDate = d => d.toISOString().slice(0, 10);
  console.log('首日:', formatDate(firstDay));
  console.log('末日:', formatDate(lastDay));
};
// 获取日期数字
const getDate = date => {
  return date.getDate();
};
// 获取对应日期的事件
const getEvents = dateString => {
  //  console.log('000',dateString);
  return events.value[dateString] || [];
};
const getJobEventBar = () => {
  const monthRange = getCurrentMonthRange();
  params.value = monthRange;
  console.log(monthRange);
  getCalen(params.value).then(res => {
    if (res.data.code !== 0) return;
    // events.value = res.data.data
    console.log('日历', res.data.data);
  });
};
onMounted(() => {
  getJobEventBar();
});
</script>
<style lang="scss" scoped>
.calenBox {
  margin-top: 10px;
  height: 546px;
  overflow: hidden;
  .event-item {
    font-size: 12px;
    padding: 2px;
    margin: 2px 0;
    border-radius: 3px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    &.work-order {
      // background: #ecf5ff;
      color: #409eff;
      span {
        display: inline-block;
        width: 7px;
        height: 7px;
        background: #1c5cff;
        border-radius: 50%;
        margin-right: 2px;
      }
    }
    &.task {
      // background: #f0f9eb;
      color: #67c23a;
      span {
        display: inline-block;
        width: 7px;
        height: 7px;
        background: #029d36;
        border-radius: 50%;
        margin-right: 2px;
      }
    }
  }
}
</style>
src/views/wel/components/flyratio.vue
New file
@@ -0,0 +1,198 @@
<template>
  <div class="machineNest">
    <div class="nestTop">
      <div class="card-title">
        <img :src="jc1" alt="" />
        <div class="cardtotal">
          <p>机巢事件数量排名</p>
          <!-- <div class="total-number">111</div>
          <span>个</span> -->
        </div>
      </div>
      <!-- <div class="status-grid">
        <div v-for="(item, index) in jcOrder" :key="index" class="status-item">
          <img :src="jc2" alt="" />
          <div>
            <div class="status-label">{{ item.name }}</div>
            <div :style="{ color: '#387FC8' }" class="status-value">
              {{ item.value }}
            </div>
          </div>
        </div>
      </div> -->
    </div>
    <div class="nestCenter">
      <div class="chart" ref="echartsRef"></div>
    </div>
  </div>
</template>
<script setup>
import * as echarts from 'echarts';
import useEchartsResize from '@/hooks/useEchartsResize';
import jc1 from '@/assets/images/workbench/jc1.png';
import jc2 from '@/assets/images/workbench/jc2.png';
import { industryJobNumPieChart } from '@/api/home/index';
const echartsRef = ref(null);
let { chart: jcchart } = useEchartsResize(echartsRef);
const jcOrder = ref([]);
// 获取机巢事件数据
const getIndustryJobNumPieChart = value => {
  industryJobNumPieChart(value).then(res => {
    const resList = res?.data?.data || [];
    jcOrder.value = resList;
    pieInit(resList);
  });
};
const pieInit = resList => {
  // 处理数据,过滤掉没有name的项
  const validData = resList.filter(item => item.name);
  // 准备图表数据
  const optionData = {
    yAxisData: validData.map(item => item.name),
    seriesData: validData.map(item => item.value),
  };
  const option = {
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow',
      },
      formatter: '{b}: {c}',
    },
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true,
    },
    xAxis: {
      type: 'value',
      splitLine: {
        lineStyle: {
          color: '#E5E5E5',
        },
      },
      axisLabel: {
        color: '#35455aa6',
      },
      boundaryGap: [0, 0.01],
    },
    yAxis: {
      type: 'category',
      data: optionData.yAxisData, // 使用处理后的分类数据
      axisLabel: {
        color: '#35455aa6',
      },
      axisLine: {
        lineStyle: {
          color: '#D1D1D1',
        },
      },
      axisTick: {
        show: false,
      },
    },
    series: [
      {
        type: 'bar',
        data: optionData.seriesData, // 使用处理后的数值数据
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
            { offset: 0, color: '#93BAFF' },
            { offset: 1, color: '#C5DEFF' },
          ]),
        },
        barWidth: '30%',
        label: {
          show: true,
          position: 'right',
          formatter: '{c}',
        },
      },
    ],
  };
  jcchart.value.setOption(option);
};
onMounted(() => {
  getIndustryJobNumPieChart({ date_enum: 'CURRENT_WEEK' });
});
</script>
<style scoped lang="scss">
.machineNest {
  .nestTop {
    .card-title {
      display: flex;
      margin-left: 57px;
      margin-bottom: 15px;
      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: 'YouSheBiaoTiHei';
        font-weight: bold;
        font-size: 32px;
        color: #2a54ff;
      }
    }
    .status-grid {
      margin-left: 30px;
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      row-gap: 17px;
      .status-item {
        display: flex;
        text-align: center;
        align-items: center;
        justify-content: center;
        background: #f6f8fe;
        margin-right: 10px;
        padding: 10px;
        img {
          width: 40px;
          height: 40px;
          margin-right: 13px;
        }
        .status-label {
          font-size: 14px;
          color: #383838;
        }
        .status-value {
          font-family: 'YouSheBiaoTiHei';
          font-weight: bold;
          font-size: 20px;
        }
      }
    }
  }
  .nestCenter {
    width: 100%;
    height: 600px;
    .chart {
      width: 100%;
      height: 100%;
    }
  }
}
</style>
src/views/wel/components/statistics.vue
New file
@@ -0,0 +1,447 @@
<template>
  <div class="statistics">
    <div class="title">
      <div class="name">
        <span> 设备统计</span>
        <img src="/src/assets/images/workbench/st1.png" alt="" />
      </div>
      <div class="arrow">
        <img src="/src/assets/images/workbench/st2.png" alt="" />
      </div>
    </div>
    <!-- <div class="grid-container">
      <div v-for="(item, index) in Object.keys(newtitleData)" :key="index" class="device-card">
        <div class="device-title">
          <img :src="test[item].img" :alt="newtitleData[item].name" />
          <div class="itemcenter">
            <div>{{ test[item]?.name || '--' }}</div>
            <span class="shu">{{ newtitleData[item].total_num }}</span>
            <span>个</span>
          </div>
        </div>
        <div class="status-list">
          <div
            v-for="(status, statusIndex) in newtitleData[item].status_map"
            :key="statusIndex"
            class="status-item"
            :class="getStatusStyle(test[item]?.name, statusIndex)"
            :style="{ color: getStatusColor(test[item]?.name, statusIndex) }"
          >
            <span class="indicator"></span>
            <span class="label">{{ getStatusLabel(test[item]?.name, statusIndex) }}</span>
            <span class="count"
              >{{ status }} {{ test[item]?.name === '无人机' ? '架' : '个' }}</span
            >
          </div>
        </div>
      </div>
    </div> -->
    <div class="grid-container1">
      <div v-for="(item, index) in Object.keys(newtitleData)" :key="index" class="device-card">
        <div class="device-title">
          <img :src="test[item].img" :alt="newtitleData[item].name" />
          <div class="itemcenter">
            <div>{{ test[item]?.name || '--' }}</div>
            <span class="shu">{{ newtitleData[item].total_num }}</span>
            <span>个</span>
          </div>
        </div>
        <div class="status-list">
          <!-- :class="`status-${statusIndex}`" -->
          <div
            v-for="(status, statusIndex) in newtitleData[item].status_map"
            :key="statusIndex"
            class="status-item"
            :class="getStatusStyle(test[item]?.name, statusIndex)"
            :style="{ color: getStatusColor(test[item]?.name, statusIndex) }"
          >
            <span
              class="indicator"
              :style="{
                backgroundColor: getStatusBackground(test[item]?.name, statusIndex),
                color: getStatusColor(test[item]?.name, statusIndex),
              }"
            ></span>
            <span class="label">{{ getStatusLabel(test[item]?.name, statusIndex) }}</span>
            <span class="count"
              >{{ status }} {{ test[item]?.name === '无人机' ? '架' : '个' }}</span
            >
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { useStore } from 'vuex';
import { computed } from 'vue';
import titleImg1 from '@/assets/images/workbench/st3.png';
import titleImg2 from '@/assets/images/workbench/st4.png';
import titleImg3 from '@/assets/images/workbench/st5.png';
import titleImg4 from '@/assets/images/workbench/st6.png';
import titleImg5 from '@/assets/images/workbench/st8.png';
import titleImg6 from '@/assets/images/workbench/st9.png';
import { getStatics } from '@/api/home/index';
const store = useStore();
Object.key;
const userInfo = computed(() => store.getters.userInfo);
const newtitleData = ref({});
const test = {
  no_move_list: {
    name: '机巢',
    img: titleImg1,
  },
  plant_list: {
    name: '无人机',
    img: titleImg2,
  },
  flow_type_list: {
    name: '机巢流量',
    img: titleImg3,
  },
  monitor_list: {
    name: '监控设备',
    img: titleImg4,
  },
  move_list: {
    name: '移动机巢',
    img: titleImg5,
  },
  insure_list: {
    name: '机巢保险',
    img: titleImg6,
  },
};
const statusSelect = {
  0: '空闲中',
  4: '作业中',
  '-1': '离线中',
};
// 流量 状态 0充足,1=流量到期,2=不足
const flowStatus = {
  0: '流量无忧',
  1: '流量到期',
  2: '流量不足',
};
// "监控状态 1=在线,0=离线"
const monitorStatus = {
  0: '离线中',
  1: '在线中',
};
// 机巢保险  1=保险,0=未保险
const insureStatus = {
  0: '临近到期',
  1: '正常期限',
};
// 样式配置对象
const statusStyles = {
  机巢保险: {
    0: { class: 'expired', color: '#7C8091', background: '#7C8091' },
    1: { class: 'normal', color: '#1B94FF', background: '#1B94FF' },
  },
  机巢流量: {
    0: { class: 'offline', color: '#11CE3E', background: '#11CE3E' },
    1: { class: 'flying', color: '#1B94FF', background: '#1B94FF' },
    2: { class: 'flying', color: '#7C8091', background: '#7C8091' },
  },
  监控设备: {
    0: { class: 'offline', color: '#bababa', background: '#bababa' },
    1: { class: 'flying', color: '#1B94FF', background: '#1B94FF' },
  },
  // 默认样式配置
  default: {
    0: { class: 'warning', color: '#1b94ff', background: '#1b94ff' },
    4: { class: 'success', color: '#11ce3e', background: '#11ce3e' },
    '-1': { class: 'success', color: '#bababa', background: '#bababa' },
  },
};
const getStatusStyle = (name, statusIndex) => {
  // 获取样式配置,优先使用名称专属配置
  const styleConfig = statusStyles[name] || statusStyles.default;
  return styleConfig[statusIndex]?.class || '';
};
const getStatusColor = (name, statusIndex) => {
  // 获取颜色配置,优先使用名称专属配置
  const styleConfig = statusStyles[name] || statusStyles.default;
  return styleConfig[statusIndex]?.color || '#333';
};
// 新增背景颜色获取方法
const getStatusBackground = (name, statusIndex) => {
  const styleConfig = statusStyles[name] || statusStyles.default;
  return (
    styleConfig[statusIndex]?.background ||
    styleConfig[statusIndex]?.color || // 降级使用字体颜色
    '#F0F0F0'
  ); // 最终默认颜色
};
const getStatusLabel = (itemName, statusCode) => {
  switch (itemName) {
    case '机巢流量':
      return flowStatus[statusCode] || statusSelect[statusCode];
    case '监控设备':
      return monitorStatus[statusCode] || statusSelect[statusCode];
    case '机巢保险':
      return insureStatus[statusCode] || statusSelect[statusCode];
    default:
      return statusSelect[statusCode] || `未知状态(${statusCode})`;
  }
};
const getStaticsList = () => {
  getStatics(userInfo.value.detail.areaCode).then(res => {
    newtitleData.value = res.data.data;
    console.log('设备', newtitleData.value);
  });
};
const unitMap = {
  drone: '架',
  nest: '个',
  monitor: '个',
  mobile: '个',
};
const titleData = ref([
  {
    img: titleImg1,
    name: '机巢',
    type: 'nest',
    data: 52,
    statuses: [
      { type: 'working', label: '作业中', count: 20 },
      { type: 'idle', label: '空间中', count: 20 },
      { type: 'offline', label: '离线中', count: 20 },
    ],
  },
  {
    img: titleImg2,
    name: '无人机',
    data: 52,
    type: 'drone',
    statuses: [
      { type: 'working', label: '作业中', count: 20 },
      { type: 'idle', label: '空间中', count: 20 },
      { type: 'offline', label: '离线中', count: 20 },
    ],
  },
  {
    img: titleImg3,
    name: '监控设备',
    type: 'monitor',
    data: 52,
    statuses: [
      { type: 'working', label: '作业中', count: 20 },
      { type: 'idle', label: '空间中', count: 20 },
      { type: 'offline', label: '离线中', count: 20 },
    ],
  },
  {
    img: titleImg4,
    name: '移动机巢',
    type: 'mobile',
    data: 52,
    statuses: [
      { type: 'working', label: '作业中', count: 20 },
      { type: 'idle', label: '空间中', count: 20 },
      { type: 'offline', label: '离线中', count: 20 },
    ],
  },
]);
onMounted(() => {
  getStaticsList();
});
</script>
<style scoped lang="scss">
.statistics {
  // height: 174px;
  background: #ffffff;
  border-radius: 8px 8px 8px 8px;
  margin-bottom: 10px;
  .title {
    padding: 14px 14px 0 21px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .name {
      display: flex;
      align-items: center;
      span {
        margin-right: 4px;
      }
    }
    .arrow {
      cursor: pointer;
    }
  }
  .grid-container {
    padding: 0 14px 0 21px;
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    .device-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      .device-title {
        display: flex;
        align-items: center;
        img {
          width: 43px;
          height: 44px;
        }
      }
      .itemcenter {
        margin-left: 14px;
        font-weight: 400;
        font-size: 14px;
        color: #343434;
        .shu {
          font-weight: bold;
          font-size: 36px;
          color: #363636;
        }
        span {
          font-weight: 400;
          font-size: 12px;
          color: #7c8091;
        }
      }
    }
    .status-list {
      display: grid;
      .status-item {
        display: flex;
        align-items: center;
        border-radius: 6px;
        background: #fff;
        font-size: 12px;
        gap: 10px;
        margin-bottom: 5px;
        .indicator {
          width: 6px;
          height: 6px;
          border-radius: 50%;
        }
        .label {
          flex: 1;
          // font-size: clamp(12px, 1vw, 14px);
        }
        .count {
          // font-size: clamp(14px, 1.1vw, 16px);
          font-size: 14px;
          color: #7c8091;
        }
        &.status-4 .indicator {
          background: #11ce3e;
        }
        &.status-4 {
          color: #11ce3e;
        }
        &.status-0 .indicator {
          background: #1b94ff;
        }
        &.status-0 {
          color: #1b94ff;
        }
        &.status--1 .indicator {
          background: #bababa;
        }
        &.status--1 {
          color: #bababa;
        }
      }
    }
  }
  .grid-container1 {
    padding: 0 14px 0 21px;
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    .device-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      .device-title {
        display: flex;
        align-items: center;
        img {
          width: 43px;
          height: 44px;
        }
      }
      .itemcenter {
        margin-left: 14px;
        font-weight: 400;
        font-size: 14px;
        color: #343434;
        .shu {
          font-weight: bold;
          font-size: 36px;
          color: #363636;
        }
        span {
          font-weight: 400;
          font-size: 12px;
          color: #7c8091;
        }
      }
    }
    .status-list {
      display: grid;
      .status-item {
        display: flex;
        align-items: center;
        border-radius: 6px;
        background: #fff;
        font-size: 12px;
        gap: 10px;
        margin-bottom: 5px;
        .indicator {
          width: 6px;
          height: 6px;
          border-radius: 50%;
        }
        .label {
          flex: 1;
          // font-size: clamp(12px, 1vw, 14px);
        }
        .count {
          // font-size: clamp(14px, 1.1vw, 16px);
          font-size: 14px;
          color: #7c8091;
        }
        &.status-4 .indicator {
          background: #11ce3e;
        }
        &.status-4 {
          color: #11ce3e;
        }
        &.status-0 .indicator {
          background: #1b94ff;
        }
        &.status-0 {
          color: #1b94ff;
        }
        &.status--1 .indicator {
          background: #bababa;
        }
        &.status--1 {
          color: #bababa;
        }
      }
    }
  }
}
</style>