riku
2024-05-09 75aeb4e63339b60f9559af984c7d9f87a7cba24a
轨迹动画和任务管理
已修改15个文件
已添加9个文件
1002 ■■■■ 文件已修改
src/api/index.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/missionApi.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CardDialog.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/animation/HistoricalTrajectory.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/animation/TrajectoryState.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/CoreHeader.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/CoreMenu.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/BaseMap.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/MapToolbox.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/mission/MIssionCreate.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/mission/MissionImport.vue 补丁 | 查看 | 原始文档 | blame | 历史
src/components/mission/MissionManage.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search/OptionMission.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/formConfirm.js 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/FrameAnimation.js 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/map-animation.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/mission.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/expand/expand.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/animation.js 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/sector.js 300 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/HomePage.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/historymode/HistoryMode.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js
@@ -4,6 +4,7 @@
const debug = false;
let ip1 = 'http://114.215.109.124:8805/';
// let ip1 = 'http://47.100.191.150:9029/';
if (debug) {
  ip1 = 'http://192.168.0.138:8082/';
src/api/missionApi.js
@@ -8,5 +8,14 @@
    let params = `page=${page}&perPage=${pageSize}`;
    params += type ? `&type=${type}` : '';
    return $http.get(`air/mission/type?${params}`).then((res) => res.data);
  },
  putNewMission(mission) {
    return $http.post(`air/mission/create`, mission).then((res) => res.data);
  },
  deleteMission(missionCode) {
    let params = `missionCode=${missionCode}`;
    return $http.post(`air/mission/delete?${params}`).then((res) => res.data);
  }
};
src/components.d.ts
@@ -10,6 +10,7 @@
    BaseCard: typeof import('./components/BaseCard.vue')['default']
    BaseMap: typeof import('./components/map/BaseMap.vue')['default']
    CardButton: typeof import('./components/CardButton.vue')['default']
    CardDialog: typeof import('./components/CardDialog.vue')['default']
    CoreHeader: typeof import('./components/core/CoreHeader.vue')['default']
    CoreMenu: typeof import('./components/core/CoreMenu.vue')['default']
    DataSummary: typeof import('./components/monitor/DataSummary.vue')['default']
@@ -20,6 +21,7 @@
    ElCol: typeof import('element-plus/es')['ElCol']
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDropdown: typeof import('element-plus/es')['ElDropdown']
    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -42,6 +44,9 @@
    HistoricalTrajectory: typeof import('./components/animation/HistoricalTrajectory.vue')['default']
    LineChart: typeof import('./components/monitor/LineChart.vue')['default']
    MapToolbox: typeof import('./components/map/MapToolbox.vue')['default']
    MIssionCreate: typeof import('./components/mission/MIssionCreate.vue')['default']
    MissionImport: typeof import('./components/mission/MissionImport.vue')['default']
    MissionManage: typeof import('./components/mission/MissionManage.vue')['default']
    OptionDevice: typeof import('./components/search/OptionDevice.vue')['default']
    OptionMission: typeof import('./components/search/OptionMission.vue')['default']
    OptionTime: typeof import('./components/search/OptionTime.vue')['default']
@@ -50,6 +55,7 @@
    RouterView: typeof import('vue-router')['RouterView']
    SearchBar: typeof import('./components/search/SearchBar.vue')['default']
    SliderBar: typeof import('./components/SliderBar.vue')['default']
    TrajectoryState: typeof import('./components/animation/TrajectoryState.vue')['default']
  }
  export interface ComponentCustomProperties {
    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
src/components/CardDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
<template>
  <el-dialog
    :model-value="modelValue"
    @update-modelvalue="handleChange"
    :show-close="false"
    align-center
  >
    <template #header="{ close, titleId, titleClass }">
      <BaseCard direction="top-left" borderless="t">
        <template #content>
          <el-row justify="space-between" align="middle">
            <el-row align="middle">
              <font-awesome-icon icon="fa fa-list" class="m-r-4" />
              <span :id="titleId" :class="titleClass">{{ title }}</span>
            </el-row>
            <font-awesome-icon
              icon="fa fa-times"
              class="cursor-p m-r-4"
              @click="close"
            />
          </el-row>
        </template>
      </BaseCard>
    </template>
    <BaseCard size="medium">
      <template #content>
        <slot></slot>
      </template>
    </BaseCard>
  </el-dialog>
</template>
<script>
export default {
  props: {
    title: String,
    modelValue: Boolean
  },
  emits: ['update:modelValue'],
  methods: {
    handleChange(value) {
      this.$emit('update:modelValue', value);
    }
  }
};
</script>
src/components/animation/HistoricalTrajectory.vue
@@ -17,7 +17,7 @@
        </div>
        <div class="label-date margin-left-2">
          <span class="label-date-title">倍速</span>
          <el-select v-model="speed" size="small" class="w-80">
          <el-select v-model="speed" size="small" class="w-60">
            <el-option label="1.0X" :value="1" />
            <el-option label="4.0X" :value="4" />
            <el-option label="8.0X" :value="8" />
@@ -29,15 +29,37 @@
  </BaseCard>
</template>
<script>
import { mapActions } from 'pinia';
import { MapAnimation } from '@/utils/map/animation';
import { FactorDatas } from '@/model/FactorDatas';
import { useMapAnimationStore } from '@/stores/map-animation';
const mapAnimation = new MapAnimation();
export default {
  props: {},
  emits: ['change'],
  props: {
    factorDatas: FactorDatas,
    factorType: String
  },
  emits: ['change', 'stop'],
  data() {
    return {
      speed: 1,
      // åŠ¨ç”»çŠ¶æ€ï¼Œ0:停止;1:播放;2:暂停
      status: 0
    };
  },
  watch: {
    speed(nV, oV) {
      if (nV != oV) {
        this.changeSpeed(nV);
      }
    },
    factorType(nV, oV) {
      if (nV != oV) {
        mapAnimation.setFactorType(nV);
      }
    }
  },
  computed: {
    btnStop() {
@@ -53,26 +75,58 @@
    }
  },
  methods: {
    ...mapActions(useMapAnimationStore, ['start', 'pause', 'stop']),
    handleStop() {
      if (this.status != 0) {
        this.status = 0;
        this.stopAnimation();
        this.handleChange();
      }
    },
    handlePlayOrPause() {
      if (this.status == 1) {
        this.status = 2;
        this.pauseAnimation();
      } else {
        this.status = 1;
        this.startAnimation();
      }
      this.handleChange();
    },
    handleChange() {
      console.log(this.status);
      this.$emit('change', this.status);
    },
    newTimeTask() {
      mapAnimation.setDynamicSpeed(false); //关闭动态绘制速度调整
      mapAnimation.moveAnimation(this.factorDatas, this.factorType);
    },
    startAnimation() {
      this.changeSpeed(this.speed);
      if (!mapAnimation.runStatus()) {
        this.newTimeTask();
      } else {
        mapAnimation.start();
      }
      this.start();
    },
    changeSpeed(speed) {
      mapAnimation.changeSpeed(speed);
    },
    pauseAnimation() {
      mapAnimation.pause();
      this.pause();
    },
    stopAnimation() {
      mapAnimation.stop();
      this.stop();
    }
  },
  mounted() {}
  mounted() {
    mapAnimation.setOnStopCallback(() => {
      this.$emit('stop');
    });
  }
};
</script>
<style scoped>
src/components/animation/TrajectoryState.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
<template>
  <BaseCard size="middle-s" direction="down">
    <template #content>
      <span style="margin: 36px; font-size: large">{{ stateText }}</span>
    </template>
  </BaseCard>
</template>
<script>
export default {
  props: {
    status: {
      type: Number,
      default: 0
    }
  },
  computed: {
    stateText() {
      switch (this.status) {
        case 0:
          return '轨迹动画已停止';
        case 1:
          return '轨迹动画播放中...';
        case 2:
          return '轨迹动画已暂停';
        default:
          return '轨迹动画已停止';
      }
    }
  }
};
</script>
src/components/core/CoreHeader.vue
@@ -1,6 +1,6 @@
<template>
  <div class="map-title ff-title flexbox-col align-items p-events-auto">
    <div class="map-title-content">
  <div class="map-title ff-title flexbox-col align-items">
    <div class="map-title-content p-events-auto">
      <div class="ff-border-bottom"></div>
      <div class="ff-border-top">
        <div class="ff-border-content flexbox-col align-items">
@@ -25,4 +25,8 @@
  methods: {}
};
</script>
<style scoped></style>
<style scoped>
.map-title {
  /* background-color: aliceblue; */
}
</style>
src/components/core/CoreMenu.vue
@@ -1,5 +1,5 @@
<template>
  <div class="map-mode-change p-events-auto">
  <div v-show="status == 0" class="map-mode-change p-events-auto">
    <template v-for="(item, index) in menu" :key="item.path">
      <a :class="btnClz(item.selected)" @click="navTo(index)">
        <div>{{ item.name }}</div>
@@ -41,6 +41,9 @@
</template>
<script>
import { mapState } from 'pinia';
import { useMapAnimationStore } from '@/stores/map-animation';
export default {
  data() {
    return {
@@ -69,7 +72,9 @@
      ]
    };
  },
  computed: {},
  computed: {
    ...mapState(useMapAnimationStore, ['status'])
  },
  methods: {
    btnClz(selected) {
      return (
src/components/map/BaseMap.vue
@@ -29,4 +29,15 @@
  display: none;
  opacity: 0 !important;
}
.amap-marker-label {
  font-size: 13px;
  text-align: center;
  color: white;
  background-color: transparent;
  text-shadow: black 2px 2px 2px;
  border-radius: 2px;
  border: 0px;
  padding: 4px;
}
</style>
src/components/map/MapToolbox.vue
@@ -1,6 +1,6 @@
<template>
  <el-dropdown
    class="p-events-auto dropdown-wrap"
    class="p-events-auto"
    trigger="click"
    size="small"
    @command="handleCommand"
@@ -103,12 +103,6 @@
</script>
<style scoped>
.dropdown-wrap {
  position: absolute;
  top: 10px;
  left: 2px;
}
.el-button {
  margin: initial !important;
}
src/components/mission/MIssionCreate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
<template>
  <el-button
    type="primary"
    class="el-button-custom"
    @click="dialogVisible = !dialogVisible"
  >
    æ–°å»ºä»»åŠ¡
  </el-button>
  <CardDialog v-model="dialogVisible" title="新建走航任务">
    <el-form
      :inline="false"
      :model="formObj"
      ref="formRef"
      :rules="rules"
      label-position="right"
      label-width="150px"
    >
      <slot name="form-item" :formObj="formObj"></slot>
      <el-form-item>
        <el-button
          :disabled="!edit"
          type="primary"
          @click="onSubmit"
          :loading="loading"
          >提交</el-button
        >
        <el-button v-if="useCancel" @click="onCancel">取消</el-button>
      </el-form-item>
    </el-form>
  </CardDialog>
</template>
<script setup>
import { onActivated, onDeactivated, ref, reactive, watch } from 'vue';
import missionApi from '@/api/missionApi';
import { useFormConfirm } from '@/composables/formConfirm';
import { useFetchData } from '@/composables/fetchData';
const dialogVisible = ref(false);
const { loading, fetchData } = useFetchData();
const baseRules = reactive({
  _usertype: [
    {
      required: true,
      message: '用户类型不能为空',
      trigger: 'change'
    }
  ],
  _locations: [
    {
      required: true,
      message: '行政区划不能为空',
      trigger: 'change'
    }
  ],
  _scenetype: [
    {
      required: true,
      message: '场景类型不能为空',
      trigger: 'change'
    }
  ]
});
// åˆ›å»ºä»»åŠ¡
function createMission() {}
const { formObj, formRef, edit, onSubmit, onCancel, onReset, clear } =
  useFormConfirm({
    submit: {
      do: createMission
    },
    cancel: {
      do: () => (dialogVisible.value = false)
    }
  });
</script>
src/components/mission/MissionImport.vue
src/components/mission/MissionManage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,148 @@
<template>
  <el-button
    type="primary"
    icon="Memo"
    class="el-button-custom p-events-auto"
    @click="dialogVisible = !dialogVisible"
  >
    ä»»åŠ¡ç®¡ç†
  </el-button>
  <el-dialog v-model="dialogVisible" :show-close="false" align-center>
    <template #header="{ close, titleId, titleClass }">
      <BaseCard direction="top-left" borderless="t">
        <template #content>
          <el-row justify="space-between" align="middle">
            <el-row align="middle">
              <font-awesome-icon icon="fa fa-list" class="m-r-4" />
              <span :id="titleId" :class="titleClass">走航任务管理</span>
            </el-row>
            <font-awesome-icon
              icon="fa fa-times"
              class="cursor-p m-r-4"
              @click="close"
            />
          </el-row>
        </template>
      </BaseCard>
    </template>
    <BaseCard size="medium">
      <template #content>
        <el-row class="mission-table">
          <el-col :span="20">
            <el-table
              :data="missionList"
              table-layout="fixed"
              size="small"
              :show-overflow-tooltip="true"
              border
              row-class-name="t-row"
              cell-class-name="t-cell"
              header-row-class-name="t-header-row"
              header-cell-class-name="t-header-cell"
            >
              <el-table-column
                type="index"
                label="序号"
                align="center"
                width="50"
              />
              <el-table-column
                prop="missionCode"
                label="任务编号"
                align="center"
              />
              <el-table-column
                prop="startTime"
                label="开始时间"
                align="center"
                :formatter="timeFormatter"
              />
              <el-table-column
                prop="endTime"
                label="结束时间"
                align="center"
                :formatter="timeFormatter"
              />
              <el-table-column label="管理" width="70" align="center">
                <template #default="{ row }">
                  <el-button
                    type="primary"
                    size="small"
                    class="el-button-custom"
                    @click="deleteMission(row)"
                    >删除</el-button
                  >
                </template>
              </el-table-column>
            </el-table>
          </el-col>
          <el-col :span="4" class="flex-col">
            <div>
              <el-button type="primary" class="el-button-custom">
                æ–°å»ºä»»åŠ¡
              </el-button>
            </div>
            <div>
              <el-button type="primary" class="el-button-custom">
                æ•°æ®å¯¼å…¥
              </el-button>
            </div>
            <div>
              <el-button type="primary" class="el-button-custom">
                ä¸‹è½½æ¨¡æ¿
              </el-button>
            </div>
          </el-col>
        </el-row>
      </template>
    </BaseCard>
  </el-dialog>
</template>
<script>
import moment from 'moment';
import { mapState } from 'pinia';
import { useMissionStore } from '@/stores/mission';
export default {
  props: {},
  data() {
    return {
      dialogVisible: false
    };
  },
  computed: {
    ...mapState(useMissionStore, ['missionList'])
  },
  methods: {
    createMission() {},
    deleteMission(row) {},
    timeFormatter(row, col, cellValue, index) {
      return moment(cellValue).format('YYYY-MM-DD HH:mm:ss');
    }
  }
};
</script>
<style>
.el-dialog {
  --el-dialog-bg-color: transparent !important;
  --el-dialog-title-font-size: var(--el-font-size-medium);
  --el-dialog-content-font-size: 14px;
  --el-dialog-padding-primary: 0px !important;
}
.el-dialog__title {
  color: var(--font-color);
}
</style>
<style scoped>
.flex-col {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-end;
}
.mission-table {
  height: 60vh;
}
</style>
src/components/search/OptionMission.vue
@@ -18,8 +18,10 @@
</template>
<script>
import { mapStores } from 'pinia';
import missionApi from '@/api/missionApi';
import { useFetchData } from '@/composables/fetchData';
import { useMissionStore } from '@/stores/mission';
export default {
  setup() {
@@ -37,6 +39,9 @@
      index: undefined
    };
  },
  computed: {
    ...mapStores(useMissionStore)
  },
  methods: {
    fetchMission() {
      this.fetchData((page, pageSize) => {
@@ -44,6 +49,7 @@
          .fethchMission({ type: this.type, page, pageSize })
          .then((res) => {
            this.missionList = res.data;
            this.missionStore.missionList = res.data;
            // if (this.missionList.length > 0) {
            //   this.handleChange(0);
            // }
src/composables/formConfirm.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,156 @@
import { onActivated, onDeactivated, ref, watch } from 'vue';
import { useCloned } from '@vueuse/core';
// import { useMessageBoxTip, useMessageBox } from './messageBox';
// è¡¨å•的确认和取消
export function useFormConfirm({
  defaultForm = undefined,
  submit = {
    do: () => {}
  },
  cancel = {
    do: () => {}
  },
  reset = {
    do: () => {}
  }
}) {
  if (!submit.title) submit.title = '提交';
  if (!submit.msg) submit.msg = '确认是否提交?';
  if (!cancel.title) cancel.title = '取消';
  if (!cancel.msg) cancel.msg = '是否放弃已编辑的内容?';
  // const formProps = defineProps({
  //   // æ˜¯å¦åœ¨æäº¤æˆåŠŸåŽæ¸…ç©ºè¡¨å•
  //   clearAftSubmit: Boolean
  // });
  //表单内容
  const formObj = ref(defaultForm ? defaultForm : {});
  let formObjClone = useCloned(formObj, { manual: true });
  //表单组件引用
  const formRef = ref(null);
  // è¡¨å•编辑状态
  const edit = ref(false);
  let isReset = false;
  const active = ref(true);
  // è‹¥ç»„件实例是 <KeepAlive> ç¼“存树的一部分,当组件被插入到 DOM ä¸­æ—¶è°ƒç”¨
  onActivated(() => {
    active.value = true;
  });
  // è‹¥ç»„件实例是 <KeepAlive> ç¼“存树的一部分,当组件从 DOM ä¸­è¢«ç§»é™¤æ—¶è°ƒç”¨
  onDeactivated(() => {
    active.value = false;
  });
  // å½“表单被取消激活后,清空表单数据
  watch(active, (nValue) => {
    if (!nValue) {
      clear();
    }
  });
  // è¡¨å•被修改过, æ›´æ–°ç¼–辑状态
  watch(
    formObj,
    (nv, ov) => {
      if (!isReset && nv != ov) {
        formObjClone = useCloned(nv, { manual: true });
      }
      if (!isReset && nv === ov) {
        edit.value = true;
      }
      isReset = false;
    },
    { deep: true }
  );
  // é‡ç½®è¡¨å•
  const _reset = function () {
    formRef.value.clearValidate();
    edit.value = false;
    isReset = true;
    formObj.value = useCloned(formObjClone.cloned, {
      manual: true
    }).cloned.value;
  };
  // æ¸…空表单
  const clear = function () {
    isReset = true;
    formRef.value.resetFields();
    edit.value = false;
  };
  // æäº¤æˆåŠŸåŽ
  const submited = function () {
    // if (formProps.clearAftSubmit) clear();
    edit.value = false;
    formObjClone = useCloned(formObj, { manual: true });
  };
  // æäº¤è¡¨å•
  const onSubmit = function (messageBox = true) {
    formRef.value.validate(async (valid) => {
      if (valid) {
        if (messageBox) {
          // useMessageBoxTip({
          //   confirmMsg: submit.msg,
          //   confirmTitle: submit.title,
          //   onConfirm: async () => {
          //     const res = await submit.do();
          //     submited();
          //     return res;
          //   }
          // });
        } else {
          await submit.do();
          submited();
        }
      }
    });
  };
  // å–消提交
  const onCancel = function () {
    if (edit.value) {
      // å¼¹å‡ºç¡®è®¤æ¡†
      // useMessageBox({
      //   confirmMsg: cancel.msg,
      //   confirmTitle: cancel.title,
      //   onConfirm: () => {
      //     // clear();
      //     return cancel.do();
      //   }
      // });
    } else {
      cancel.do();
    }
  };
  // é‡ç½®è¡¨å•
  const onReset = function (tips) {
    if (edit.value) {
      if (tips) {
        // å¼¹å‡ºç¡®è®¤æ¡†
        // useMessageBox({
        //   confirmMsg: '是否重置表单内容?',
        //   confirmTitle: '重置表单',
        //   onConfirm: () => {
        //     _reset();
        //     return reset.do();
        //   }
        // });
      } else {
        _reset();
        reset.do();
      }
    } else {
      reset.do();
    }
  };
  return { formObj, formRef, edit, onSubmit, onCancel, onReset, clear };
}
src/main.js
@@ -7,15 +7,14 @@
import App from './App.vue';
import router from './router';
import './utils/expand/expand';
/* import the fontawesome core */
import { library } from '@fortawesome/fontawesome-svg-core';
/* import font awesome icon component */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
/* import specific icons */
// import { all } from '@awesome.me/kit-KIT_CODE/icons';
import { fas } from '@fortawesome/free-solid-svg-icons';
// import { faTwitter, faFontAwesome } from '@fortawesome/free-brands-svg-icons'
// import { faTwitter, faFontAwesome } from '@fortawesome/free-regular-svg-icons'
/* add icons to the library */
library.add(fas);
src/model/FrameAnimation.js
@@ -118,7 +118,9 @@
        if (this.isPause) {
          return;
        }
        if (index >= t.count) {
        // ç»˜åˆ¶3D图形时,最少需要2个点才可绘制图形
        // å› æ­¤æ­¤å¤„索引只到倒数第二个点就结束
        if (index >= t.count - 1) {
          this._endTask(this.intervalFlag);
          return;
        }
src/stores/map-animation.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
export const useMapAnimationStore = defineStore('mapAnimation', () => {
  // // åŠ¨ç”»çŠ¶æ€ï¼Œ0:停止;1:播放;2:暂停
  const status = ref(0);
  function start() {
    status.value = 1;
  }
  function pause() {
    status.value = 2;
  }
  function stop() {
    status.value = 0;
  }
  return { status, start, pause, stop };
});
src/stores/mission.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
// èµ°èˆªä»»åŠ¡
export const useMissionStore = defineStore('mission', () => {
  const missionList = ref([]);
  return { missionList };
});
src/utils/expand/expand.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
Date.prototype.format = function (fmt) {
  var o = {
    'M+': this.getMonth() + 1, //月份
    'd+': this.getDate(), //日
    'h+': this.getHours(), //小时
    'm+': this.getMinutes(), //分
    's+': this.getSeconds(), //秒
    'q+': Math.floor((this.getMonth() + 3) / 3), //季度
    S: this.getMilliseconds() //毫秒
  };
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(
      RegExp.$1,
      (this.getFullYear() + '').substr(4 - RegExp.$1.length)
    );
  }
  for (var k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(
        RegExp.$1,
        RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
      );
    }
  }
  return fmt;
};
src/utils/map/animation.js
@@ -3,7 +3,7 @@
import Layer from '@/utils/map/3dLayer';
import sector from '@/utils/map/sector';
import { map } from '@/utils/map/index_old';
import util from "@/utils/map/util";
import util from '@/utils/map/util';
import car_driving from '@/assets/mipmap/car_driving.png';
import boat_driving from '@/assets/mipmap/boat_driving.png';
@@ -11,7 +11,7 @@
  // éžè¿žç»­åæ ‡ç‚¹æœ€å¤§è·ç¦»(ç±³)
  this.maxD = 500;
  // å½“前绘制的监测因子类型
  this.factorType = 0;
  this.factorType;
  this.factorDatas;
  // è½½å…·ç±»åž‹
  this.vehicleType = 0; // 0: è½¦è¾†ï¼›1:无人机:2:无人船
@@ -116,7 +116,7 @@
      var fData2 = factorDatas.getByIndex(i + 1, i + 2);
      // è®¡ç®—动画轨迹
      for (let i = 0; i < count - 1; i++) {
      for (let i = 0; i < count; i++) {
        // path
        var length = d * (i + 1);
        if (isNaN(angle)) {
@@ -149,26 +149,16 @@
        animationData,
        function (data, index, count) {
          var length = data.length();
          var start = length - count + 1;
          var start = length - count;
          // 1.获取数据
          var d = data.getByIndex(0, start + index + 1);
          var f = d.factor[that.factorType + 1 + ''];
          var f = d.factor[that.factorType];
          // 2.绘制图形
          if (length > count || index > 0) {
            // 3d图形
            // var lnglat = d.lnglats_GD[d.lnglats_GD.length - 1];
            Layer.drawMesh(d, f);
            // MapUtil.drawLine(lnglat)
            // é£Žå‘风速
            sector.drawSector(d, start + index);
            // if (d.factor['17'] != undefined && d.factor['16'] != undefined) {
            //   var windDir = d.factor['17'].datas;
            //   windDir = windDir[windDir.length - 1].factorData;
            //   var windSpeed = d.factor['16'].datas;
            //   windSpeed = windSpeed[windSpeed.length - 1].factorData;
            //   MapUtil.drawSector4(lnglat, windDir, windSpeed);
            // }
            sector.drawSectorAna(d, start + index);
          }
          var pos = d.lnglats_GD[d.lnglats_GD.length - 1];
src/utils/map/sector.js
@@ -4,42 +4,30 @@
var _defaultDeg = 30,
  _sector = undefined,
  _sectorViews = {};
  _sectorViews = new Map(),
  // åŠ¨ç”»è½¨è¿¹
  _sectorViewsAna = new Map();
export default {
  clearSector() {
    var list = [];
    for (const key in _sectorViews) {
      list.push(_sectorViews[key]);
    }
    if (list.length > 0) {
      map.remove(list);
      _sectorViews = {};
    }
    if (_sector) {
      object3Dlayer.remove(_sector);
    }
  },
  drawSector(fDatas, i) {
const zoomStyleMapping = {
  14: 0,
  15: 0,
  16: 0,
  17: 0,
  18: 0,
  19: 0,
  20: 0
};
function sectorParams(fDatas, i) {
    const lnglat = fDatas.lnglats_GD[i];
    let windDir = fDatas.factor[17].datas[i].factorData;
    let windSpeed = fDatas.factor[16].datas[i].factorData;
    if (!windDir) windDir = 0;
    if (!windSpeed) windSpeed = 0;
    if (windSpeed > 10) {
      return;
    }
    if (_sector != undefined) {
      this.clearSector();
    }
    // eslint-disable-next-line no-undef
    var sector = new AMap.Object3D.Mesh();
    sector.transparent = true;
    sector.backOrFront = 'both';
    var unit = 5;
  // if (windSpeed > 10) {
  //   return;
  // }
    var sDeg = windDir - _defaultDeg; //扇形起始角度(以上方作为0度)
    // sDeg = sDeg < 0 ? sDeg + 360 : sDeg
@@ -58,11 +46,80 @@
    var lnglat2_4 = calculate.getLatLon(lnglat, distance2, eDeg);
    var list2 = calculate.parse2LngLat([lnglat2_2, lnglat2_3, lnglat2_4]);
  distance = distance.toFixed(0);
  distance2 = distance2.toFixed(0);
  return { sDeg, eDeg, lnglat, distance, distance2, list, list2 };
}
/**
 * å¯ç¼©æ”¾çš„æ ‡è®°
 * æ— æ³•修改position(官网未找到相关api)
 */
function elasticMarker(position, content) {
  // eslint-disable-next-line no-undef
  return new AMap.ElasticMarker({
    zoom: [14, 20],
    position: position,
    styles: [
      {
        icon: {
          img: imgLocation,
          size: [16, 16], //可见区域的大小
          ancher: [8, 16], //锚点
          fitZoom: 18, //最合适的级别
          scaleFactor: 1, //地图放大一级的缩放比例系数
          maxScale: 2, //最大放大比例
          minScale: 1 //最小放大比例
        },
        label: {
          content: content,
          offset: [-35, 0],
          position: 'BM',
          minZoom: 15
        }
      }
    ],
    zoomStyleMapping: zoomStyleMapping
  });
}
/**
 * æ–‡æœ¬æ ‡è®°
 * å¯ä¿®æ”¹position
 */
function textMaker(position, text) {
  // eslint-disable-next-line no-undef
  return new AMap.Text({
    text: text,
    position: position,
    style: {
      'font-size': '13px',
      'text-align': 'center',
      color: 'white',
      'background-color': 'transparent',
      'text-shadow': 'black 2px 2px 2px',
      'border-radius': '2px',
      border: '0px',
      padding: '4px'
    }
  });
}
function drawSectorMesh(sDeg, eDeg, lnglat, distance, distance2) {
  // eslint-disable-next-line no-undef
  var sector = new AMap.Object3D.Mesh();
  sector.transparent = true;
  sector.backOrFront = 'both';
  var unit = 5;
    var p0 = calculate.lngLatToGeodeticCoord([lnglat])[0];
    var geometry = sector.geometry;
    var count = distance / unit;
    var unitDeg = (eDeg - sDeg) / count;
    for (let i = 0; i < count; i++) {
      var angle1 = sDeg + unitDeg * i;
      var angle2 = sDeg + unitDeg * (i + 1);
@@ -106,127 +163,88 @@
    }
    object3Dlayer.add(sector);
    _sector = sector;
}
    distance = distance.toFixed(0);
    distance2 = distance2.toFixed(0);
    const zoomStyleMapping = {
      14: 0,
      15: 0,
      16: 0,
      17: 0,
      18: 0,
      19: 0,
      20: 0
    };
function drawTextMaker(list, list2, distance, distance2) {
    //10分钟扇形
    // eslint-disable-next-line no-undef
    var text15 = new AMap.ElasticMarker({
      zoom: [14, 20],
      position: list[2],
      styles: [
        {
          icon: {
            img: imgLocation,
            size: [16, 16], //可见区域的大小
            ancher: [8, 16], //锚点
            fitZoom: 18, //最合适的级别
            scaleFactor: 1, //地图放大一级的缩放比例系数
            maxScale: 2, //最大放大比例
            minScale: 1 //最小放大比例
          },
          label: {
            content: '<div>10分钟</div>',
            offset: [-35, 0],
            position: 'BM',
            minZoom: 15
  const a = _sectorViews.get('text10-t');
  if (a == undefined) {
    const text10t = textMaker(list[2], '10分钟');
    _sectorViews.set('text10-t', text10t);
    const textM10t = textMaker(list[1], distance + 'm');
    _sectorViews.set('textM10-t', textM10t);
    map.add([text10t, textM10t]);
  } else {
    _sectorViews.get('text10-t').setPosition(list[2]);
    _sectorViews.get('textM10-t').setPosition(list[1]);
    _sectorViews.get('textM10-t').setText(distance + 'm');
  }
  //5分钟扇形
  const b = _sectorViews.get('text5-t');
  if (b == undefined) {
    const text5t = textMaker(list2[1], '5分钟');
    _sectorViews.set('text5-t', text5t);
    const textM5t = textMaker(list2[0], distance2 + 'm');
    _sectorViews.set('textM5-t', textM5t);
    map.add([text5t, textM5t]);
  } else {
    _sectorViews.get('text5-t').setPosition(list2[1]);
    _sectorViews.get('textM5-t').setPosition(list2[0]);
    _sectorViews.get('textM5-t').setText(distance2 + 'm');
          }
        }
      ],
      zoomStyleMapping: zoomStyleMapping
    });
    _sectorViews['text10'] = text15;
    // eslint-disable-next-line no-undef
    var textM = new AMap.ElasticMarker({
      zoom: [14, 20],
      position: list[1],
      styles: [
        {
          icon: {
            img: imgLocation,
            size: [16, 16], //可见区域的大小
            ancher: [8, 16], //锚点
            fitZoom: 18, //最合适的级别
            scaleFactor: 1, //地图放大一级的缩放比例系数
            maxScale: 2, //最大放大比例
            minScale: 1 //最小放大比例
          },
          label: {
            content: `<div>${distance}m</div>`,
            offset: [-35, 0],
            position: 'BM',
            minZoom: 15
          }
        }
      ],
      zoomStyleMapping: zoomStyleMapping
    });
    _sectorViews['textM'] = textM;
    map.add(_sectorViews['text10']);
    map.add(_sectorViews['textM']);
function drawElasticMarker(list, list2, distance, distance2) {
  //10分钟扇形
  const text10 = elasticMarker(list[2], '<div>10分钟</div>');
  _sectorViews.set('text10', text10);
  const textM = elasticMarker(list[1], `<div>${distance}m</div>`);
  _sectorViews.set('textM10', textM);
  map.add([text10, textM]);
    //5分钟扇形
    let pList = list2;
    // eslint-disable-next-line no-undef
    var text5 = new AMap.ElasticMarker({
      position: pList[1],
      styles: [
        {
          icon: {
            img: imgLocation,
            size: [16, 16], //可见区域的大小
            ancher: [8, 16], //锚点
            fitZoom: 18, //最合适的级别
            scaleFactor: 1, //地图放大一级的缩放比例系数
            maxScale: 2, //最大放大比例
            minScale: 1 //最小放大比例
  const text5 = elasticMarker(list2[1], '<div>5分钟</div>');
  _sectorViews.set('text5', text5);
  const textM5 = elasticMarker(list2[0], `<div>${distance2}m</div>`);
  _sectorViews.set('textM5', textM5);
  map.add([text5, textM5]);
}
export default {
  clearSector() {
    var list = [];
    for (const iterator of _sectorViews) {
      list.push(iterator[1]);
    }
    for (const iterator of _sectorViewsAna) {
      list.push(iterator[1]);
    }
    if (list.length > 0) {
      map.remove(list);
      _sectorViews.clear();
    }
    this.clearSectorMesh();
          },
          label: {
            content: `<div>5分钟</div>`,
            offset: [-35, 0],
            position: 'BM',
            minZoom: 15
  clearSectorMesh() {
    if (_sector) {
      object3Dlayer.remove(_sector);
          }
        }
      ],
      zoomStyleMapping: zoomStyleMapping
    });
    _sectorViews['text5'] = text5;
    // eslint-disable-next-line no-undef
    var textM5 = new AMap.ElasticMarker({
      position: pList[0],
      styles: [
        {
          icon: {
            img: imgLocation,
            size: [16, 16], //可见区域的大小
            ancher: [8, 16], //锚点
            fitZoom: 18, //最合适的级别
            scaleFactor: 1, //地图放大一级的缩放比例系数
            maxScale: 2, //最大放大比例
            minScale: 1 //最小放大比例
          },
          label: {
            content: `<div>${distance2}m</div>`,
            offset: [-35, 0],
            position: 'BM',
            minZoom: 15
  drawSector(fDatas, i) {
    if (_sector != undefined) {
      this.clearSector();
          }
        }
      ],
      zoomStyleMapping: zoomStyleMapping
    });
    _sectorViews['textM5'] = textM5;
    map.add(_sectorViews['textM5']);
    map.add(_sectorViews['text5']);
    const { sDeg, eDeg, lnglat, distance, distance2, list, list2 } =
      sectorParams(fDatas, i);
    drawSectorMesh(sDeg, eDeg, lnglat, distance, distance2);
    drawElasticMarker(list, list2, distance, distance2);
  },
  drawSectorAna(fDatas, i) {
    this.clearSectorMesh();
    const { sDeg, eDeg, lnglat, distance, distance2, list, list2 } =
      sectorParams(fDatas, i);
    drawSectorMesh(sDeg, eDeg, lnglat, distance, distance2);
    drawTextMaker(list, list2, distance, distance2);
  }
};
src/views/HomePage.vue
@@ -2,7 +2,10 @@
  <BaseMap></BaseMap>
  <div class="overlay-container">
    <CoreHeader></CoreHeader>
    <el-row class="dropdown-wrap">
    <MapToolbox></MapToolbox>
      <MissionManage></MissionManage>
    </el-row>
    <CoreMenu></CoreMenu>
    <router-view></router-view>
  </div>
@@ -23,4 +26,11 @@
  /* padding: 4px; */
  pointer-events: none;
}
.dropdown-wrap {
  position: absolute;
  top: 10px;
  left: 2px;
  gap: 4px;
}
</style>
src/views/historymode/HistoryMode.vue
@@ -1,10 +1,12 @@
<template>
  <div class="p-events-none m-t-2">
    <el-row v-show="status == 0" justify="center">
    <el-row justify="center" align="middle" class="top-wrap">
      <SearchBar
        v-show="status == 0"
        :search-time="searchTime"
        @search="fetchHistroyData"
      ></SearchBar>
      <TrajectoryState v-show="status != 0" :status="status"></TrajectoryState>
    </el-row>
    <el-row class="m-t-2">
      <FactorRadio
@@ -17,6 +19,14 @@
        class="m-t-2"
        :factor="factorDatas.factor[factorType]"
      ></FactorLegend>
    </el-row>
    <el-row class="historical" justify="center">
      <HistoricalTrajectory
        :factor-datas="factorDatas"
        :factor-type="factorType"
        @change="(e) => (status = e)"
        @stop="draw"
      ></HistoricalTrajectory>
    </el-row>
    <TrendAnalysis
      class="trend-analysis"
@@ -32,11 +42,6 @@
      :factor-datas="factorDatas"
      :device-type="deviceType"
    ></DataSheet>
    <el-row class="historical" justify="center">
      <HistoricalTrajectory
        @change="(e) => (status = e)"
      ></HistoricalTrajectory>
    </el-row>
  </div>
</template>
@@ -82,7 +87,7 @@
  },
  watch: {
    factorType(nValue, oValue) {
      if (nValue != oValue) {
      if (nValue != oValue && this.status == 0) {
        this.draw();
      }
    }
@@ -115,10 +120,6 @@
      marks.drawMassMarks(this.factorDatas, e, (index) => {
        // æŸ¥è¯¢èŒƒå›´å†…的监测站点
        // SceneUtil.searchByCoordinate(lnglat[0], lnglat[1], distance);
        // 3. è¶‹åŠ¿å›¾è·³è½¬å®šä½
        // const progress = FChart.locate(lineChart.chart, lineChart.option, i, _factor.factorName);
        // 4. è¡¨æ ¼æ•°æ®è·³è½¬å®šä½
        // Table.locate(i);
        this.drawSector(index);
        this.locateIndex = index;
      });
@@ -188,6 +189,10 @@
};
</script>
<style scoped>
.top-wrap {
  height: 40px;
}
.trend-analysis {
  position: absolute;
  left: 0;
@@ -202,7 +207,7 @@
.historical {
  position: absolute;
  bottom: 0;
  bottom: 10px;
  left: 0;
  right: 0;
}