Browse Source

【修改】

master
xiaosi 7 months ago
parent
commit
af636f6edf
  1. 4
      .env
  2. 9
      .eslintrc.cjs
  3. 2
      index.html
  4. 614
      package-lock.json
  5. 6
      package.json
  6. 38
      src/assets/DroneIcon.js
  7. 19
      src/assets/bg.svg
  8. BIN
      src/assets/list_chosen.png
  9. BIN
      src/assets/list_icon_jiaciguanli.png
  10. BIN
      src/assets/list_icon_jinfeiquguanli.png
  11. BIN
      src/assets/list_icon_shebeiguanli.png
  12. BIN
      src/assets/list_icon_shishijiankong.png
  13. BIN
      src/assets/list_icon_zhizaoshangguanli.png
  14. BIN
      src/assets/live_cancel.png
  15. BIN
      src/assets/m_logo.png
  16. BIN
      src/assets/map-tan-1-1.png
  17. BIN
      src/assets/map-tan-1-2.png
  18. BIN
      src/assets/map-tan-1.png
  19. BIN
      src/assets/map-tan.png
  20. BIN
      src/assets/regulator_log.png
  21. BIN
      src/assets/top.png
  22. 34
      src/components/BasePanel.vue
  23. 9
      src/components/ImageUploader.vue
  24. 42
      src/components/ManufacturerSelector.vue
  25. 92
      src/components/MultipleFilesUploader.vue
  26. 31
      src/components/TextRow.vue
  27. 143
      src/components/TrackPlaybackController.vue
  28. 65
      src/components/TrackTimelyInfo.vue
  29. 295
      src/components/VideoPlayer.vue
  30. 38
      src/config/urls.js
  31. 264
      src/core/Enclosure.js
  32. 34
      src/core/IconPoint/Icon.js
  33. 234
      src/core/IconPoint/IconPointRenderer.js
  34. 138
      src/core/Pinpoint.js
  35. 185
      src/core/Ruler.js
  36. 455
      src/core/ShapeGroupRenderer.js
  37. 63
      src/core/blockHelper/BlockHelper.js
  38. 23
      src/core/blockHelper/blockRenderer.js
  39. 23
      src/core/blockHelper/groupRenderer.js
  40. 26
      src/core/blockHelper/subFarmRenderer.js
  41. 356
      src/core/clusterControlHelper.js
  42. 308
      src/core/deviceCruise.js
  43. 17
      src/core/obstacleHelper/ObstacleHelper.js
  44. 31
      src/core/obstacleHelper/blockObstacleRenderer.js
  45. 31
      src/core/obstacleHelper/obstacleRenderer.js
  46. 443
      src/core/trackRenderer.js
  47. 27
      src/layout/MainContainer.vue
  48. 116
      src/layout/MapLayer.vue
  49. 108
      src/layout/components/LiveDialog.vue
  50. 90
      src/layout/components/MultifunctionalBar.vue
  51. 105
      src/layout/components/PasswordEditor.vue
  52. 161
      src/layout/components/SideMenu.vue
  53. 148
      src/layout/components/TopBar.vue
  54. 2
      src/main.js
  55. 2
      src/plugins/TDesign-vue.js
  56. 34
      src/plugins/components.js
  57. 4
      src/plugins/directives.js
  58. 24
      src/router/index.js
  59. 6
      src/stores/index.js
  60. 111
      src/stores/modules/achievementStore.js
  61. 23
      src/stores/modules/authStore.js
  62. 100
      src/stores/modules/deviceStore.js
  63. 110
      src/stores/modules/liveStore.js
  64. 8
      src/stores/modules/manufacturerStore.js
  65. 55
      src/stores/modules/mediaStore.js
  66. 124
      src/stores/modules/monitorStore.js
  67. 8
      src/stores/modules/noFlyZoneStore.js
  68. 1
      src/stores/modules/regulatorStore.js
  69. 87
      src/stores/modules/sortieStore.js
  70. 36
      src/styles/common.less
  71. 466
      src/styles/theme.css
  72. 9766
      src/views/AchievementView/AchievementView.vue
  73. 226
      src/views/AchievementView/components/ImageBar.vue
  74. 178
      src/views/AchievementView/components/MediaManage.vue
  75. 173
      src/views/AchievementView/components/SortieDetail.vue
  76. 147
      src/views/DeviceView/DeviceView.vue
  77. 117
      src/views/DeviceView/components/DeviceEditor.vue
  78. 82
      src/views/ExampleView/ExampleView.vue
  79. 16
      src/views/ManufacturerView/ManufacturerView.vue
  80. 4
      src/views/ManufacturerView/components/ManufacturerEditor.vue
  81. 1674
      src/views/MonitorView/MonitorView.vue
  82. 105
      src/views/MonitorView/components/DeviceStatBox.vue
  83. 303
      src/views/MonitorView/components/DroneDetail.vue
  84. 1
      src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue
  85. 21
      src/views/NoFlyZoneView/NoFlyZoneView.vue
  86. 11
      src/views/RegulatorView/RegulatorView.vue
  87. 7
      src/views/RegulatorView/components/RegulatorEditor.vue
  88. 9731
      src/views/SortieView/SortieView.vue
  89. 229
      src/views/SortieView/components/ImageBar.vue
  90. 178
      src/views/SortieView/components/MediaManage.vue
  91. 173
      src/views/SortieView/components/SortieDetail.vue
  92. 6
      src/views/common/useGlobalSettings.js
  93. 19
      vite.config.js

4
.env

@ -1,4 +1,4 @@
APP_TITLE=云端无人机管理系统
APP_ICON_URL=//at.alicdn.com/t/c/font_4349903_2p0gyrtbo2.js
APP_ICON_URL=//at.alicdn.com/t/c/font_4349903_nbo0vtlaa9d.js
APP_DEVELOPMENT_BASE_URL=/api
APP_PRODUCTION_BASE_URL=/
APP_PRODUCTION_BASE_URL=http://sgcloud-test.jiagutech.com/api

9
.eslintrc.cjs

@ -9,7 +9,16 @@ module.exports = {
],
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 'latest',
sourceType: 'module',
requireConfigFile: false,
allowImportExportEverywhere: true,
babelOptions: {
parserOpts: {
plugins: ['jsx'],
},
},
},
settings: {

2
index.html

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" theme-mode="dark">
<html lang="en" theme-mode="light">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">

614
package-lock.json

File diff suppressed because it is too large

6
package.json

@ -17,6 +17,7 @@
"gcoord": "^1.0.5",
"jwt-decode": "^3.1.2",
"less": "^4.2.0",
"lodash.throttle": "^4.1.1",
"mapbox-gl": "^2.15.0",
"mitt": "^3.0.1",
"mobx": "^6.12.0",
@ -26,9 +27,12 @@
"popmotion": "^11.0.5",
"tdesign-vue-next": "^1.7.0",
"vue": "^3.3.4",
"vue-router": "^4.2.5"
"vue-router": "^4.2.5",
"xgplayer": "^3.0.10",
"xgplayer-hls": "^3.0.10"
},
"devDependencies": {
"@babel/eslint-parser": "^7.23.3",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"eslint": "^8.49.0",

38
src/assets/DroneIcon.js

@ -0,0 +1,38 @@
/**
* 飞机图标
*/
class DroneIcon {
constructor(colorOption) {
const {
outlineColor = 'rgba(255, 255, 255, 1)',
bodyColor = 'rgba(255, 255, 255, 0.3)',
directionColor = 'rgba(255, 255, 255, 1)',
} = colorOption || {};
this.width = 128;
this.height = 128;
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
const context = canvas.getContext('2d');
/* eslint-disable max-len */
const outlinePath = new Path2D();
outlinePath.addPath(new Path2D('M100.3,94.6c-0.6-0.9-1.1-1.7-1.8-2.5c-0.3-0.4-0.6-0.8-0.9-1.2s-0.6-0.8-1-1.1c-1.3-1.5-2.7-3-4.1-4.3 c-0.7-0.7-1.4-1.4-2.1-2l-2.2-2l-4.3-4.1L82.4,76V52l1.4-1.3l4.3-4.1l2.2-2c0.7-0.7,1.4-1.3,2.2-2c1.4-1.5,2.8-2.9,4.1-4.4 c0.3-0.4,0.7-0.8,1-1.2s0.6-0.8,0.9-1.2c0.6-0.8,1.2-1.6,1.8-2.5c0.5-0.8,0.3-1.9-0.5-2.4c-0.5-0.3-1.1-0.3-1.7-0.1 c-0.9,0.5-1.8,1-2.7,1.5c-0.4,0.3-0.8,0.5-1.3,0.8s-0.8,0.6-1.2,0.8c-1.6,1.1-3.2,2.3-4.7,3.6c-0.8,0.6-1.5,1.3-2.3,1.9l-2.2,2 L81.9,43c0-0.1-0.1-0.2-0.2-0.3l-4.1-7.1c-1.1-1.9-3.1-3-5.2-3H55.8c-2.2,0-4.1,1.1-5.2,3l-4.1,7.1c-0.1,0.1-0.2,0.3-0.2,0.4l-2-1.8 l-2.2-2c-0.7-0.7-1.5-1.3-2.2-1.9c-1.5-1.3-3.1-2.5-4.7-3.6L33.9,33c-0.4-0.3-0.8-0.5-1.3-0.8c-0.8-0.5-1.7-1-2.6-1.5 c-0.8-0.4-1.9-0.1-2.3,0.8c-0.3,0.5-0.2,1.2,0.1,1.6c0.5,0.9,1.1,1.7,1.7,2.5c0.3,0.4,0.6,0.8,0.9,1.2s0.6,0.8,1,1.2 c1.3,1.5,2.7,3,4.1,4.3c0.7,0.7,1.4,1.4,2.1,2l2.2,2l4.3,4.1l1.5,1.6v23.6L44,77.1l-4.3,4.1l-2.2,2c-0.7,0.7-1.4,1.3-2.1,2 c-1.4,1.4-2.8,2.8-4.1,4.3c-0.3,0.4-0.6,0.8-1,1.1s-0.6,0.8-0.9,1.2c-0.6,0.8-1.2,1.6-1.8,2.5c-0.5,0.8-0.3,1.9,0.5,2.4l0,0 c0.5,0.3,1.1,0.3,1.7,0.1c0.9-0.5,1.8-1,2.6-1.5l1.3-0.8l1.2-0.8c1.6-1.2,3.2-2.4,4.7-3.6c0.8-0.6,1.5-1.3,2.3-1.9l2.2-2l1.8-1.6 c0.1,0.3,0.3,0.6,0.4,0.9l4.1,7.1c1.1,1.9,3.1,3,5.2,3h16.5c2.2,0,4.2-1.1,5.2-3l4.1-7.1c0.1-0.3,0.3-0.5,0.4-0.8l1.7,1.4l2.2,2 c0.8,0.7,1.5,1.3,2.3,1.9c1.5,1.3,3.1,2.5,4.7,3.6l1.2,0.8l1.3,0.8c0.8,0.5,1.7,1,2.6,1.5c0.8,0.4,1.9,0.1,2.3-0.7 C100.5,95.7,100.5,95,100.3,94.6L100.3,94.6z M79.1,82.8c0,0.5-0.1,0.9-0.4,1.4l-4.1,7.1c-0.5,0.9-1.4,1.4-2.4,1.4H55.8 c-1,0-1.9-0.5-2.4-1.4l-4.1-7.1c-0.2-0.4-0.4-0.9-0.4-1.3v-37c0-0.1,0-0.2,0-0.2c0-0.4,0.2-0.8,0.3-1.1l4.1-7.1 c0.5-0.9,1.4-1.4,2.4-1.4h16.5c1,0,1.9,0.5,2.4,1.4l4.1,7.1c0.2,0.3,0.3,0.6,0.3,1c0,0.1,0,0.3,0,0.4L79.1,82.8z'));
outlinePath.addPath(new Path2D('M29.8,44.2c-0.3,0-0.7,0-1,0c-6.7,0-12.1-5.4-12.1-12.1C16.6,25.5,22,20,28.7,20c6.6,0,12,5.3,12.1,11.9 l3.1,2.8c0.2-0.9,0.2-1.7,0.2-2.6c0-8.5-6.9-15.3-15.4-15.3c-8.4,0-15.3,6.9-15.3,15.4s6.9,15.3,15.4,15.3c1.4,0,2.7-0.2,4-0.5 L29.8,44.2z M99.1,80.5c-1.4,0-2.8,0.2-4.1,0.6l3.1,2.8c0.3,0,0.7,0,1,0c6.7,0,12.1,5.4,12.1,12.1s-5.4,12.1-12.1,12.1 S87,102.7,87,96.1l-3.1-2.9c-0.1,0.9-0.2,1.7-0.2,2.6c0,8.5,6.9,15.4,15.4,15.4s15.4-6.9,15.4-15.4C114.4,87.4,107.6,80.5,99.1,80.5 L99.1,80.5L99.1,80.5z M99.1,16.8c-8.5,0-15.3,6.9-15.4,15.3c0,0.9,0.1,1.7,0.2,2.6l3.1-2.8c0.1-6.7,5.6-12,12.2-11.9 s12,5.6,11.9,12.2S105.6,44.1,99,44.1c-0.3,0-0.7,0-1,0L95,47c1.3,0.4,2.7,0.5,4.1,0.5c8.5,0,15.4-6.9,15.4-15.4 S107.6,16.8,99.1,16.8L99.1,16.8L99.1,16.8z M40.8,96c-0.1,6.7-5.6,12-12.2,11.9s-12-5.6-11.9-12.2c0.1-6.6,5.5-11.9,12.1-11.9 c0.4,0,0.7,0,1,0l3.1-2.8c-1.3-0.4-2.7-0.5-4.1-0.6c-8.5,0-15.4,6.9-15.4,15.4s6.9,15.4,15.4,15.4s15.3-6.9,15.4-15.3 c0-0.9-0.1-1.8-0.2-2.6L40.8,96z'));
context.fillStyle = outlineColor;
context.fill(outlinePath);
const bodyPath = new Path2D('M79,46c0-0.1,0-0.3,0-0.4c0-0.4-0.1-0.7-0.3-1l-4.1-7.1c-0.5-0.9-1.4-1.4-2.4-1.4H55.7c-1,0-1.9,0.5-2.4,1.4 l-4.1,7.1c-0.1,0.3-0.3,0.7-0.3,1.1c0,0,0,0.1,0,0.2v37c0,0.4,0.2,0.9,0.4,1.3l4.1,7.1c0.5,0.9,1.4,1.4,2.4,1.4h16.4 c1,0,1.9-0.5,2.4-1.4l4.1-7.1c0.3-0.5,0.4-0.9,0.4-1.4L79,46z M67.3,55.2c0,1.8-1.5,3.3-3.3,3.3s-3.3-1.5-3.3-3.3v-8.8 c0-1.8,1.5-3.3,3.3-3.3s3.3,1.5,3.3,3.3V55.2z');
context.fillStyle = bodyColor;
context.fill(bodyPath);
const directionPath = new Path2D('M64,43.1c-1.8,0-3.3,1.5-3.3,3.3v8.8c0,1.8,1.5,3.3,3.3,3.3s3.3-1.5,3.3-3.3v-8.8C67.3,44.6,65.8,43.1,64,43.1z');
context.fillStyle = directionColor;
context.fill(directionPath);
/* eslint-enable max-len */
this.data = context.getImageData(0, 0, this.width, this.height).data;
}
}
export default DroneIcon;

19
src/assets/bg.svg

@ -0,0 +1,19 @@
<svg width="162.000000" height="203.000000" viewBox="0 0 162 203" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<filter id="filter_3_2_dd" x="0.000000" y="0.000000" width="162.000000" height="203.000000" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feGaussianBlur in="BackgroundImage" stdDeviation="16.6667"/>
<feComposite in2="SourceAlpha" operator="in" result="effect_backgroundBlur_1"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect_backgroundBlur_1" result="shape"/>
</filter>
</defs>
<g filter="url(#filter_3_2_dd)">
<path id="矢量 1" d="M11 2L1 2L1 1L2 1L2 11L0 11L0 0L11 0L11 2Z" fill-rule="evenodd" fill="#2B74FF"/>
<path id="矢量 2" d="M11 201L1 201L1 202L2 202L2 192L0 192L0 203L11 203L11 201Z" fill-rule="evenodd" fill="#2B74FF"/>
<path id="矢量 3" d="M151 201L161 201L161 202L160 202L160 192L162 192L162 203L151 203L151 201Z" fill-rule="evenodd" fill="#2B74FF"/>
<path id="矢量 4" d="M151 2L161 2L161 1L160 1L160 11L162 11L162 0L151 0L151 2Z" fill-rule="evenodd" fill="#2B74FF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/list_chosen.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

BIN
src/assets/list_icon_jiaciguanli.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

BIN
src/assets/list_icon_jinfeiquguanli.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/list_icon_shebeiguanli.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

BIN
src/assets/list_icon_shishijiankong.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

BIN
src/assets/list_icon_zhizaoshangguanli.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/live_cancel.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/assets/m_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
src/assets/map-tan-1-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/map-tan-1-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/map-tan-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
src/assets/map-tan.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
src/assets/regulator_log.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
src/assets/top.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

34
src/components/BasePanel.vue

@ -1,6 +1,5 @@
<script setup>
import { ref, useSlots } from 'vue';
// import { useRoute, useRouter } from 'vue-router';
const emit = defineEmits(['toggle']);
@ -25,9 +24,9 @@
type: Boolean,
default: false,
},
expandAbility: {
showTip: {
type: Boolean,
default: false,
default: true,
},
});
@ -63,7 +62,7 @@
</script>
<template>
<div :class="[s.root, isOpen ? 'is-open' : '', expandAbility ? 'expand' : '']" :style="{ '--width': width, 'min-width': minWidth }">
<div :class="[s.root, isOpen ? 'is-open' : '']" :style="{ '--width': width, 'min-width': minWidth }">
<div class="wrapper">
<div class="header" v-if="!!header || !!headerExtra">
<t-space size="small">
@ -82,8 +81,8 @@
<slot name="footer" />
</div>
</div>
<div class="nav" v-if="expandAbility">
<t-tooltip placement="left" :delay="0" :content="isOpen ? '收起' : '展开'">
<div class="nav">
<t-tooltip placement="left" :delay="0" :content="showTip ? (isOpen ? '收起' : '展开') : ''">
<div class="btn" @click="onToggle">
<t-icon name="chevron-right-double" v-if="isOpen" />
<t-icon name="chevron-left-double" v-else />
@ -100,20 +99,21 @@
right: 0;
top: 0;
bottom: 0;
//z-index: 1;
z-index: 2;
color: var(--td-text-color-primary);
width: var(--width);
background-color: var(--td-bg-color-page);
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
box-sizing: border-box;
transition: all 0.2s ease;
transform: translateX(100%);
&:global.expand{
transform: translateX(100%);
}
//&:global.expand{
// transform: translateX(100%);
//}
&:global.is-open {
transform: translateX(0px);
transform: translateX(0%);
}
:global {
@ -141,16 +141,17 @@
border-radius: var(--td-radius-medium);
box-sizing: border-box;
//box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%);
box-shadow: 0 0 1px 0 var(--td-component-border);
//box-shadow: 0 0 1px 0 var(--td-component-border);
border: 1px solid var(--td-component-stroke);
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
.interspace {
flex: 1;
}
.t-space .t-icon {
font-size: var(--td-font-size-headline-medium);
}
//.t-space .t-icon {
// font-size: var(--td-font-size-headline-medium);
//}
}
.top-bar {
@ -162,7 +163,8 @@
//padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
overflow: auto;
//box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%);
box-shadow: 0 0 1px 0 var(--td-component-border);
//box-shadow: 0 0 1px 0 var(--td-component-border);
border: 1px solid var(--td-component-stroke);
background-color: var(--td-bg-color-container);
border-radius: var(--td-radius-medium);
}

9
src/components/ImageUploader.vue

@ -87,6 +87,15 @@
.t-upload__card-name {
display: none
}
.t-upload__card-image {
width: 100%;
height: 100%;
.t-image {
object-fit: cover;
}
}
}
}
</style>

42
src/components/ManufacturerSelector.vue

@ -0,0 +1,42 @@
<script setup>
import { computed, ref } from 'vue';
import { useManufacturerStore } from '@/stores';
import { MessagePlugin } from 'tdesign-vue-next';
const manufacturerStore = useManufacturerStore();
const { getManufacturerList } = manufacturerStore;
const list = ref([]);
const loading = ref(false);
function loadList() {
loading.value = true;
getManufacturerList({ page: 1, pageSize: 10, all: true, search: undefined }, { mergeArgs: false, mergeData: false }).then(({ data = [] } = {}) => {
// const { data: manufacturerList = [] } = data || {};
// console.log(data);
list.value = data;
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
}).finally(() => {
loading.value = false;
});
}
loadList();
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const value = computed({
get() {
return props.modelValue;
},
set(nv) {
emit('update:modelValue', nv);
},
});
const options = computed(() => list.value.map(({ manufacturerName, id }) => ({ label: manufacturerName, value: id })));
</script>
<template>
<t-select v-model="value" :loading="loading" :options="options" placeholder="请选择制造商" filterable />
</template>

92
src/components/MultipleFilesUploader.vue

@ -0,0 +1,92 @@
<script setup>
import { ref, watch } from 'vue';
import { REQUEST_UPLOAD_FILE } from '@/config/urls';
import auth from '@/utils/auth';
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:modelValue']);
const subject = ref('image');
const files = ref([]);
watch(() => props.modelValue, (nv = []) => {
console.log('nv', nv);
if (!nv.length) files.value = [];
if (files.value.length) return;
files.value = [...props.modelValue];
}, { deep: true });
const ABRIDGE_NAME = [10, 7];
const formatResponse = (res) => {
if (!res) {
return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' };
}
return res;
};
function beforeUpload(UploadFile) {
const { type = 'image' } = UploadFile || {};
if (type.includes('image')) {
subject.value = 'image';
}
if (type.includes('video')) {
subject.value = 'video';
}
return true;
}
// const handleSuccess = ({ response }) => {
const handleSuccess = () => {
setTimeout(() => {
const temp = files.value.map(({ name, size, status, url, response, type }) => ({
name,
size,
status,
url: url || response?.data?.url,
raw: {},
type,
}));
emit('update:modelValue', temp);
});
};
const handleRemove = ({ index }) => {
const temp = [...(props.modelValue || [])];
temp.splice(index, 1);
emit('update:modelValue', temp);
};
</script>
<template>
<t-upload
v-model="files"
placeholder="支持批量上传文件,文件格式不限"
:action="REQUEST_UPLOAD_FILE(subject)"
:headers="{
Authorization: `Bearer ${auth.getToken()}`,
}"
theme="file-flow"
multiple
:abridge-name="ABRIDGE_NAME"
auto-upload
show-thumbnail
allow-upload-duplicate-file
:format-response="formatResponse"
:before-upload="beforeUpload"
@success="handleSuccess"
@remove="handleRemove"
/>
</template>
<style lang="less" module="s">
.root {
//
}
</style>

31
src/components/TextRow.vue

@ -0,0 +1,31 @@
<script setup>
defineProps({
label: {
type: String,
default: '',
},
});
</script>
<template>
<dl :class="s.root">
<dt v-if="label">{{ label }}</dt>
<dd><slot /></dd>
</dl>
</template>
<style lang="less" module="s">
.root {
display: flex;
margin: 0;
&:global > dt {
margin: 0;
}
&:global > dd {
margin: 0;
flex: 1;
}
}
</style>

143
src/components/TrackPlaybackController.vue

@ -0,0 +1,143 @@
<script setup>
import { computed } from 'vue';
import deviceCruise from '@/core/deviceCruise';
import { popularTime } from '@/utils/helpers';
const ready = computed(() => deviceCruise.ready);
const speedRate = computed(() => deviceCruise.speedRate);
const totalTime = computed(() => deviceCruise.totalTime);
const currentTime = computed({
get() {
return deviceCruise.elapsedMs;
},
set(val) {
deviceCruise.setCurrentTime(val);
},
});
// const currentTime = ref(0);
// deviceCruise.change((elapsedMs = 0) => {
// currentTime.value = elapsedMs;
// });
// const currentTime = ref(0);
// watch(() => device.value.elapsedMs, (nv) => {
// console.log(nv);
// });
const showPlayButton = computed(() => deviceCruise.isPaused || deviceCruise.isStopped);
const sliderMarks = computed(() => ({ [totalTime.value]: popularTime(totalTime.value / 1000, 1) }));
const handleTipFormat = computed(() => popularTime(deviceCruise.elapsedMs / 1000, 1) || 0);
function onChangeSpeedRate() {
const speedRat = speedRate.value >= 8 ? 1 : speedRate.value * 2;
deviceCruise.setSpeedRate(speedRat);
}
function onPlay() {
deviceCruise.handlePlay();
}
function onPause() {
deviceCruise.handlePause();
}
function onStop() {
deviceCruise.handleStop();
}
</script>
<template>
<div :class="$style.root" v-if="ready">
<t-slider
v-model="currentTime"
:tooltip-props="{ visible: false, content: `${handleTipFormat}`, placement: 'top' }"
:max="totalTime" :marks="sliderMarks"
/>
<div class="btn speedRate" @click="onChangeSpeedRate">
<span>{{ speedRate }}</span>
<t-icon size="large" name="forward" />
</div>
<t-icon name="play" size="large" class="btn" v-if="showPlayButton" @click="onPlay" />
<t-icon name="pause" size="large" class="btn" v-else @click="onPause" />
<t-icon name="stop" size="large" class="btn" @click="onStop" />
</div>
</template>
<style lang="less" module>
//@import "~@/styles/variables";
.root {
position: absolute;
left: 20px;
right: 20px;
bottom: 20px;
display: flex;
align-items: center;
background-color: fade(#eee, 90%);
padding: 20px;
border-radius: 5px;
:global {
.t-slider {
//flex: 1;
//margin: 0 30px 0 0;
//
//&.ant-slider-with-marks {
// margin-bottom: 0;
//
// .ant-slider-mark-text {
// color: @primary-color;
// }
//}
.t-slider__rail {
background-color: fade(white, 90%);
}
&:hover .t-slider__rail {
background-color: fade(#4582e6, 40%);
}
//.t-slider__track {
// background-color: #4582e6;
//}
//
.t-slider__button {
background-color: var(--td-brand-color);
border-color: white;
box-shadow: 0 0 0 5px fade(#4582e6, 40%);
}
//
//.ant-slider-handle {
// border-color: fade(white, 80%);
// background-color: @primary-color;
// box-shadow: 0 0 0 5px fade(@primary-color, 20%);
//}
}
.speedRate {
display: flex;
//font-size: 14px;
line-height: 1;
user-select: none;
}
//.t-icon {
// font-size: 16px;
//}
.btn {
border: 1px solid var(--td-brand-color);
border-radius: 3px;
padding: 0 5px;
color: var(--td-brand-color);
margin-left: 10px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
}
}
</style>

65
src/components/TrackTimelyInfo.vue

@ -0,0 +1,65 @@
<script setup>
import deviceCruise from '@/core/deviceCruise';
import TextRow from '@/components/TextRow.vue';
import { computed } from 'vue';
import { formatTime, toFixed } from '@/utils/helpers';
const info = computed(() => deviceCruise.timelyData);
const isVisible = computed(() => !!Object.keys(info.value).length);
const isBox = computed(() => {
const { yaw } = info.value;
return yaw === undefined;
});
</script>
<template>
<div :class="s.root" v-if="isVisible">
<template v-if="isBox">
<TextRow label="时间:">{{ formatTime(info.timestamp) }}</TextRow>
<TextRow label="经度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lng, 7) }}</ValueUnit></TextRow>
<TextRow label="纬度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lat, 7) }}</ValueUnit></TextRow>
<TextRow label="速度:"><ValueUnit unit="米/秒" tag="small">{{ toFixed(info.spd / 10, 2) }}</ValueUnit></TextRow>
</template>
<template v-else>
<TextRow label="时间:">{{ formatTime(info.timestamp) }}</TextRow>
<TextRow label="经度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lng, 7) }}</ValueUnit></TextRow>
<TextRow label="纬度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lat, 7) }}</ValueUnit></TextRow>
<TextRow label="对地高度:"><ValueUnit unit="米" tag="small">{{ toFixed(info.height, 2) }}</ValueUnit></TextRow>
<TextRow label="水平速度:"><ValueUnit unit="米/秒" tag="small">{{ toFixed(info.xspeed, 2) }}</ValueUnit></TextRow>
<TextRow label="垂直速度:"><ValueUnit unit="米/秒" tag="small">{{ toFixed(info.yspeed, 2) }}</ValueUnit></TextRow>
<TextRow label="航向角:"><ValueUnit unit="°" tag="small">{{ toFixed(info.yaw, 2) }}</ValueUnit></TextRow>
<TextRow label="横滚角:"><ValueUnit unit="°" tag="small">{{ toFixed(info.ra, 2) }}</ValueUnit></TextRow>
<TextRow label="俯仰角:"><ValueUnit unit="°" tag="small">{{ toFixed(info.pa, 2) }}</ValueUnit></TextRow>
<TextRow label="喷洒流速:"><ValueUnit unit="升/分钟" tag="small">{{ toFixed(info.flowSpeed, 2) }}</ValueUnit></TextRow>
<TextRow label="已施药量:"><ValueUnit unit="毫升" tag="small">{{ toFixed(info.dose, 2) }}</ValueUnit></TextRow>
</template>
</div>
</template>
<style lang="less" module="s">
.root {
position: absolute;
right: 10px;
top: 10px;
//background-color: fade(black, 60%);
background-color: fade(#eee, 90%);
//color: whitesmoke;
color: black;
padding: 20px;
border-radius: 5px;
font-size: 14px;
:global {
dl dt {
width: 70px;
text-align: right;
}
dt {
//color: var(--td-brand-color);
}
}
}
</style>

295
src/components/VideoPlayer.vue

@ -0,0 +1,295 @@
<script setup>
import BasePanel from '@/components/BasePanel.vue';
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
// import HlsPlugin from 'xgplayer-hls';
import { onMounted, ref, onUnmounted, watch } from 'vue';
// import MultipleFilesUploader from '@/components/MultipleFilesUploader.vue';
// import mp4 from '@/assets/xgplayer-demo.mp4';
// import liveCancel from '@/assets/live_cancel.png';
// let player = new Player({
// id: 'mse',
// url: '//abc.com/**/*.mp4',
// height: '100%',
// width: '100%',
// });
const props = defineProps({
list: {
type: Array,
default: () => [],
},
});
const playerRef = ref();
const player = ref();
onMounted(() => {
const options = {
// id: 'player',
el: playerRef.value,
url: '', // HLS
// type: 'hls',
// fluid: true,
autoplayMuted: false,
autoplay: false,
lang: 'zh',
width: '100%',
height: '100%',
};
player.value = new Player(options);
// if (props.list.length) {
// setTimeout(() => {
// player.value.setConfig({
// url: props.list[0]?.url,
// });
// });
// // onPlayNext(props.list[0]);
// }
});
const panelOpen = ref(false);
function onPlayNext(media) {
player.value.playNext({
url: media.url,
autoplay: true,
// poster: '',
});
}
onUnmounted(() => {
if (player.value) player.value.destroy();
});
watch(() => props.list, (nv) => {
if (!nv.length) {
player.value.playNext({
url: [],
autoplay: false,
// poster: liveCancel,
});
return;
}
// onPlayNext(nv[0]);
if (!player.value) return;
onPlayNext(nv[0]);
});
</script>
<template>
<div :class="s.root">
<div ref="playerRef" />
<BasePanel
:open="panelOpen"
class="list-container"
width="25vh"
style="z-index: 50"
expand-ability
:show-tip="false"
scrollbar
>
<div class="title">视频列表</div>
<t-list>
<template v-if="list.length">
<t-list-item v-for="item in list" :key="item.id">
{{ item.name }}
<template #action>
<t-button variant="text" @click="onPlayNext(item)">
<template #icon>
<t-icon name="play-circle" />
</template>
</t-button>
</template>
</t-list-item>
</template>
<template v-else>
<t-list-item>
<div style="text-align: center;">还没有上传视频哦 ~</div>
</t-list-item>
</template>
</t-list>
</BasePanel>
</div>
</template>
<style lang="less" module="s">
.root {
position: relative;
height: 100%;
overflow: hidden;
//pointer-events: none;
//background-color: fade(red, 80%);
&:global:hover {
.list-container {
display: inline-block;
}
}
:global {
.list-container {
display: none;
padding: 0;
.nav {
top: 50%;
transform: translateY(-60%);
}
.nav .btn {
width: unset;
padding: 40px 2px;
}
.title {
padding: var(--td-comp-paddingTB-s);
text-align: center;
border-bottom: 1px solid var(--td-component-stroke);
}
}
//.urlList {
// pointer-events: auto;
// position: absolute;
// right: 0;
// top: 0;
// bottom: 0;
// z-index: 2;
// color: var(--td-text-color-primary);
// width: var(--width);
// background-color: var(--td-bg-color-page);
// padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
// box-sizing: border-box;
// transition: all 0.2s ease;
//
// &.expand{
// transform: translateX(100%);
// }
//
// &.is-open {
// transform: translateX(0px);
// }
//
// .wrapper {
// height: 100%;
// //background-color: var(--td-bg-color-container);
// background-color: transparent;
// //border-radius: var(--td-radius-medium);
// //border-radius: var(--td-radius-medium);
// //border-left-color: var(--td-gray-color-10);
// //border-left: 1px solid var(--td-component-stroke);
// //backdrop-filter: blur(20px);
// display: flex;
// flex-direction: column;
// //overflow: hidden;
//
// .header {
// display: flex;
// justify-content: center;
// align-items: center;
// min-height: var(--td-comp-size-xxl);
// font-size: var(--td-font-size-title-large);
// background-color: var(--td-bg-color-container);
// //background-color: fade(black, 30%);
// border-radius: var(--td-radius-medium);
// box-sizing: border-box;
// //box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%);
// //box-shadow: 0 0 1px 0 var(--td-component-border);
// border: 1px solid var(--td-component-stroke);
// padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
//
// .interspace {
// flex: 1;
// }
//
// .t-space .t-icon {
// font-size: var(--td-font-size-headline-medium);
// }
// }
//
// .top-bar {
// padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
// }
//
// .body {
// flex: 1;
// //padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
// overflow: auto;
// //box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%);
// //box-shadow: 0 0 1px 0 var(--td-component-border);
// border: 1px solid var(--td-component-stroke);
// background-color: var(--td-bg-color-container);
// border-radius: var(--td-radius-medium);
// }
//
// .footer {
// display: flex;
// justify-content: center;
// align-items: center;
// min-height: var(--td-comp-size-xxl);
// padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
// //background-color: fade(black, 30%);
// box-sizing: border-box;
// }
// }
//
// .nav {
// position: absolute;
// right: 100%;
// //left: 100%;
// top: var(--td-comp-paddingTB-m);
//
// .btn {
// width: var(--td-comp-size-l);
// min-height: var(--td-comp-size-m);
// padding: var(--td-comp-paddingTB-m) 0;
// background-color: var(--td-bg-color-container);
// border-right: 1px solid var(--td-bg-color-container);
// border-radius: var(--td-radius-medium) 0 0 var(--td-radius-medium);
// backdrop-filter: blur(10px);
// transition: background-color ease 0.2s;
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// box-sizing: border-box;
// cursor: pointer;
//
// &:hover {
// background-color: var(--td-brand-color-hover);
// }
//
// & + .btn {
// border-top: 1px solid fade(black, 50%);
// }
//
// span {
// display: none;
// width: 1em;
// font-size: var(--td-font-size-link-medium);
// line-height: 1.2;
// margin-top: 5px;
// }
//
// &.is-active {
// background-color: var(--td-brand-color-active);
// border-top-color: transparent;
//
// span {
// display: block;
// }
// }
// }
// }
//}
}
}
</style>

38
src/config/urls.js

@ -13,6 +13,9 @@ export const REQUEST_UPLOAD_FILE = (subject) => buildURL(`${BASE_URL}/fileServ/v
// 登录
export const LOGIN_WITH_PASSWORD = `${BASE_URL}/userServ/v1/login`;
// 更改密码
export const UPDATE_PASSWORD = `${BASE_URL}/userServ/v1/users/{id}`;
// 禁飞区管理
export const GET_NO_FLY_ZONE_LIST = `${BASE_URL}/mainServ/v1/noFlyZones`;
export const GET_NO_FLY_ZONE_DETAIL = `${BASE_URL}/mainServ/v1/noFlyZones/{id}`;
@ -28,9 +31,42 @@ export const UPDATE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
export const DELETE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
export const UPDATE_MANUFACTURER_DISABLE = `${BASE_URL}/mainServ/v1/manufacturers/{id}/disable`;
// 制造商管理
// 监管者管理
export const GET_REGULATOR_LIST = `${BASE_URL}/userServ/v1/regulators`;
export const CREATE_REGULATOR = `${BASE_URL}/userServ/v1/regulator`;
// export const UPDATE_REGULATOR = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
// export const DELETE_REGULATOR = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
// export const UPDATE_MANUFACTURER_DISABLE = `${BASE_URL}/mainServ/v1/manufacturers/{id}/disable`;
// 设备管理
export const GET_DEVICE_LIST = `${BASE_URL}/mainServ/v1/devices`;
export const GET_DEVICE_DETAIL = `${BASE_URL}/mainServ/v1/devices/{id}`;
export const CREATE_DEVICE = `${BASE_URL}/mainServ/v1/devices`;
export const UPDATE_DEVICE = `${BASE_URL}/mainServ/v1/devices/{id}`;
export const DELETE_DEVICE = `${BASE_URL}/mainServ/v1/devices/{id}`;
export const UPDATE_DEVICE_LOCKED = `${BASE_URL}/mainServ/v1/devices/{id}/locked`;
// 实时监控
export const GET_ONLINE_DEVICE = `${BASE_URL}/flightServ/v1/dashboard/onlineDrones`;
// 架次管理
export const GET_SORTIE_LIST = `${BASE_URL}/flightServ/v1/sorties`;
export const GET_SORTIE_DETAIL = `${BASE_URL}/flightServ/v1/sorties/{id}`;
// export const CREATE_SORTIE = `${BASE_URL}/mainServ/v1/devices`;
// export const UPDATE_SORTIE = `${BASE_URL}/mainServ/v1/devices/{id}`;
// export const DELETE_SORTIE = `${BASE_URL}/mainServ/v1/devices/{id}`;
// export const UPDATE_SORTIE_LOCKED = `${BASE_URL}/mainServ/v1/devices/{id}/locked`;
// 媒体
export const GET_MEDIA_LIST = `${BASE_URL}/flightServ/v1/sorties/{id}/medias`;
export const CREATE_MEDIA = `${BASE_URL}/flightServ/v1/sorties/{id}/medias`;
export const DELETE_MEDIA = `${BASE_URL}/flightServ/v1/sorties/medias/{id}`;
// 成果管理
export const GET_ACHIEVEMENT_LIST = `${BASE_URL}/flightServ/v1/sorties/outcomes`;
export const CREATE_ACHIEVEMENT_SHARE_CODE = `${BASE_URL}/flightServ/v1/sorties/shareCode`;
export const BINDING_ACHIEVEMENT = `${BASE_URL}/flightServ/v1/sorties/outcomes/{id}`;
// export const DELETE_MEDIA = `${BASE_URL}/flightServ/v1/sorties/medias/{id}`;
// 直播
export const GET_PULL_STREAM_URL = `${BASE_URL}/flightServ/v1/sorties/pullStreamUrl/{id}`;

264
src/core/Enclosure.js

@ -0,0 +1,264 @@
/**
* 圈地算面积
*/
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
const SOURCE_LINE = 'enclosure-source-line';
const SOURCE_FILL = 'enclosure-source-fill';
const SOURCE_SYMBOL = 'enclosure-source-symbol';
const LAYER_LINE = 'enclosure-layer-line';
const LAYER_FILL = 'enclosure-layer-fill';
const LAYER_SYMBOL = 'enclosure-layer-symbol';
class Enclosure {
// 地图实例
map = null;
// 点坐标
coordinates = [];
// 操作点
markers = [];
// 形状是否已经闭合(即:是否已经圈完地了)
isClosed = false;
// 形状是否扭结了
isKinked = false;
// 视觉选项
option = {
edgeColor: 'rgba(0, 0, 255, 0.8)',
fillColor: 'rgba(0, 0, 255, 0.3)',
markerColor: 'rgba(255, 255, 255, 0.8)',
labelColor: 'rgba(255, 255, 255, 0.8)',
labelStrokeColor: 'rgba(0, 0, 0, 1)',
labelFontSize: 14,
};
constructor(map, option = {}) {
this.map = map;
this.option = {
...this.option,
...option,
};
}
onDrawOn() {
this.map.getCanvas().style.cursor = 'crosshair';
this.coordinates = [];
this.markers = [];
this.isClosed = false;
this.map.on('click', this.onClickMap);
this.map.on('style.load', this.draw);
this.map.fire('enclosure.on');
}
onDrawOff() {
this.map.getCanvas().style.cursor = '';
if (this.map.getSource(SOURCE_LINE)) {
this.map.removeLayer(LAYER_LINE);
this.map.removeSource(SOURCE_LINE);
}
if (this.map.getSource(SOURCE_FILL)) {
this.map.removeLayer(LAYER_FILL);
this.map.removeSource(SOURCE_FILL);
}
if (this.map.getSource(SOURCE_SYMBOL)) {
this.map.removeLayer(LAYER_SYMBOL);
this.map.removeSource(SOURCE_SYMBOL);
}
this.markers.forEach(m => m.remove());
this.map.off('click', this.onClickMap);
this.map.off('style.load', this.draw);
this.map.fire('enclosure.off');
}
// 生成线段geojson
genLineFeature() {
const [first] = this.coordinates;
const coordinates = this.isClosed ? [...this.coordinates, first] : this.coordinates;
return turf.lineString(coordinates);
}
// 生成多边形geojson
// eslint-disable-next-line class-methods-use-this
genPolygonFeature(lineFeature) {
return turf.lineToPolygon(lineFeature);
}
// 检测形状是否扭结(判断新坐标加入后,或者对原有坐标进行判断)
checkKinked(newCoordinate) {
if (this.coordinates.length < 3) return false;
if (newCoordinate) {
const lineFeature = turf.lineString([...this.coordinates, newCoordinate]);
const kinks = turf.kinks(lineFeature);
return !!kinks.features.length;
}
const lineFeature = turf.lineString(this.coordinates);
const polygonFeature = turf.lineToPolygon(lineFeature);
const kinks = turf.kinks(polygonFeature);
return !!kinks.features.length;
}
draw = () => {
const { length: count } = this.coordinates;
if (count < 2) return;
// 显示边线
const sourceLine = this.map.getSource(SOURCE_LINE);
const lineFeature = this.genLineFeature();
if (!sourceLine) {
this.map.addSource(SOURCE_LINE, {
type: 'geojson',
data: lineFeature,
});
this.map.addLayer({
id: LAYER_LINE,
type: 'line',
source: SOURCE_LINE,
paint: {
'line-color': this.option.edgeColor,
'line-width': 2,
},
});
} else {
sourceLine.setData(lineFeature);
}
if (count < 3) return;
// 显示多边形
const sourceFill = this.map.getSource(SOURCE_FILL);
const polygonFeature = this.genPolygonFeature(lineFeature);
if (!sourceFill) {
this.map.addSource(SOURCE_FILL, {
type: 'geojson',
data: polygonFeature,
});
this.map.addLayer({
id: LAYER_FILL,
type: 'fill',
source: SOURCE_FILL,
paint: {
'fill-color': this.option.fillColor,
},
});
} else {
sourceFill.setData(polygonFeature);
}
if (!this.isClosed) return;
const area = this.isKinked ? '0' : (turf.area(polygonFeature) * 0.0015).toFixed(2); // 亩
const pointFeature = turf.centerOfMass(polygonFeature);
pointFeature.properties.area = this.isKinked ? '' : `${area}`;
const sourceSymbol = this.map.getSource(SOURCE_SYMBOL);
if (!sourceSymbol) {
this.map.addSource(SOURCE_SYMBOL, {
type: 'geojson',
data: pointFeature,
});
this.map.addLayer({
id: LAYER_SYMBOL,
type: 'symbol',
source: SOURCE_SYMBOL,
layout: {
'text-field': '{area}',
'text-anchor': 'top',
'text-size': this.option.labelFontSize,
'text-allow-overlap': true,
},
paint: {
'text-color': this.option.labelColor,
'text-halo-color': this.option.labelStrokeColor,
'text-halo-width': 1,
},
});
} else {
sourceSymbol.setData(pointFeature);
}
};
// 闭合路径
closePath() {
this.isClosed = true;
if (this.checkKinked()) {
this.isKinked = true;
this.map.fire('enclosure.kinked', { action: 'drag' });
} else {
this.isKinked = false;
}
this.draw();
}
// 生成操作点
genMarkerNode() {
const node = document.createElement('div');
node.style.cursor = 'pointer';
node.style.width = '12px';
node.style.height = '12px';
node.style.borderRadius = '50%';
node.style.background = this.option.markerColor;
node.style.boxSizing = 'border-box';
node.style.border = `2px solid ${this.option.edgeColor}`;
node.addEventListener('click', e => {
e.stopPropagation();
const [first] = this.markers;
// 当有3个以上的点出现,就可以点击第一个点,闭合多边形
if (first && (first.getElement() === node) && this.coordinates.length >= 3) {
this.closePath();
}
}, false);
return node;
}
onClickMap = e => {
if (this.isClosed) return;
const { lng, lat } = e.lngLat;
// 判断新坐标是否会造成扭结(即:新的边线是否会跟其他边线交叉)
if (this.checkKinked([lng, lat])) {
this.map.fire('enclosure.kinked', { action: 'click' });
return;
}
const marker = new mapbox.Marker({
element: this.genMarkerNode(),
draggable: true,
}).setLngLat(e.lngLat).addTo(this.map);
this.coordinates.push([lng, lat]);
this.draw();
this.markers.push(marker);
this.map.fire('enclosure.change', { coordinates: this.coordinates });
marker.on('drag', () => {
const index = this.markers.indexOf(marker);
const { lng: newLng, lat: newLat } = marker.getLngLat();
this.coordinates[index] = [newLng, newLat];
if (this.checkKinked()) {
this.isKinked = true;
this.map.fire('enclosure.kinked', { action: 'drag' });
} else {
// 从扭结复原了
if (this.isKinked) {
this.map.fire('enclosure.kink-recovered');
}
this.isKinked = false;
}
this.draw();
});
marker.on('dragend', () => {
this.map.fire('enclosure.change', { coordinates: this.coordinates });
});
};
}
export default Enclosure;

34
src/core/IconPoint/Icon.js

@ -0,0 +1,34 @@
/**
* 图标
*/
const check = Symbol('Icon.check');
class Icon {
constructor(name, image, zoom, offset) {
Icon[check](image);
this.name = name;
this.zoom = zoom || 1;
this.offset = offset || [0, 0];
this.data = image;
}
update(image) {
Icon[check](image);
this.data = image;
}
static [check](image) {
if (
!(image instanceof HTMLImageElement)
&& !(image instanceof ImageBitmap)
&& !(image instanceof ImageData)
&& !('data' in image && 'width' in image && 'height' in image)
) {
throw new Error('image类型错误');
}
}
}
export default Icon;

234
src/core/IconPoint/IconPointRenderer.js

@ -0,0 +1,234 @@
/**
* 带图标的点
*/
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
import EventDispatcher from '../EventDispatcher';
import Icon from './Icon';
class IconPointRenderer extends EventDispatcher {
_topic = null;
_map = null;
_icons = {};
_dataSource = [];
get _sourceId() {
return `${this._topic}-icon-point-source`;
}
get _layerId() {
return `${this._topic}-icon-point-layer`;
}
get _pointFeatures() {
return this._dataSource.map(({ id, iconName, point, ...others }) => {
const { [iconName]: icon } = this._icons;
const { zoom: iconZoom = 1, offset: iconOffset = [0, 0] } = icon || {};
const feature = turf.point(point, { id, iconName, iconZoom, iconOffset, ...others });
feature.id = id;
return feature;
});
}
get _pointFeatureCollection() {
return turf.featureCollection(this._pointFeatures);
}
constructor(topic, icons = []) {
super(['mousemove', 'mouseleave', 'mouseenter']);
this._topic = topic;
if (!icons.length) {
throw new Error('入参icons至少包含一个图标元素');
}
icons.forEach(this.updateIcon);
makeObservable(this, {
_topic: observable,
_icons: observable,
_dataSource: observable,
_sourceId: computed,
_layerId: computed,
_pointFeatures: computed,
_pointFeatureCollection: computed,
loadDataSource: action,
updateIcon: action,
destroy: action,
});
reaction(() => this._dataSource, () => {
this._render();
});
reaction(() => this._icons, () => {
this._render();
});
}
setMap(mapInstance) {
if (this._map === mapInstance) return;
if (!(mapInstance instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this._map = mapInstance;
}
// 载入数据
// list->item必须包含id、point、iconName(其中point如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
// list->item可以包含rotate,表示旋转角度
loadDataSource(list) {
if (!this._map) {
throw new Error('请先设置地图实例');
}
this._dataSource = (list || []).map(item => {
const { id, iconName, point } = item;
const newItem = { ...item };
if (!Array.isArray(point)) {
const { lng, lat } = point;
newItem.point = [lng, lat];
}
return (id >= 0 && iconName && Array.isArray(newItem.point)) ? newItem : null;
}).filter(Boolean);
if (!this._dataSource.length) {
console.warn('数据列表为空');
}
}
updateIcon = icon => {
if (!(icon instanceof Icon)) {
throw new Error(`入参icon必须是${Icon}的实例`);
}
this._icons = {
...this._icons,
[icon.name]: icon,
};
};
_loadIcons() {
if (!this._map) return;
Object.values(this._icons).forEach(icon => {
if (this._map.hasImage(icon.name)) {
this._map.updateImage(icon.name, icon.data);
} else {
this._map.addImage(icon.name, icon.data);
}
});
}
_render() {
if (!this._map) return;
this._loadIcons();
this._renderPoints();
}
_renderPoints() {
const source = this._map.getSource(this._sourceId);
if (!source) {
this._map.addSource(this._sourceId, {
type: 'geojson',
data: this._pointFeatureCollection,
});
this._map.addLayer({
id: this._layerId,
type: 'symbol',
source: this._sourceId,
paint: {
'icon-opacity': [
'case',
['boolean', ['feature-state', 'visible'], true],
1,
0,
],
// 'icon-translate': ['get', 'iconOffset'], // 缩放之后平移的像素值(mark:icon-translate不支持表达式)
},
layout: {
'icon-image': ['get', 'iconName'],
'icon-size': ['get', 'iconZoom'],
'icon-offset': ['get', 'iconOffset'], // 乘以缩放值后,才是最终偏移量
'icon-rotate': ['coalesce', ['get', 'rotate'], ['number', 0]],
'icon-allow-overlap': true,
},
});
this._map.on('click', this._layerId, this._onClick);
this._map.on('mouseenter', this._layerId, this._onMouseEnter);
this._map.on('mousemove', this._layerId, this._onMouseMove);
this._map.on('mouseleave', this._layerId, this._onMouseLeave);
} else {
source.setData(this._pointFeatureCollection);
}
}
_onClick = e => {
if (!e.features.length) return;
const { lngLat, point } = e;
const [{ properties: detail }] = e.features;
this._trigger('click', { lngLat, point, detail });
};
_onMouseMove = () => {
this._map.getCanvas().style.cursor = 'pointer';
};
_onMouseEnter = e => {
if (!e.features.length) return;
const { lngLat, point } = e;
const [{ properties: detail }] = e.features;
this._trigger('mouseenter', { lngLat, point, detail });
};
_onMouseLeave = e => {
this._map.getCanvas().style.cursor = '';
const { lngLat, point } = e;
this._trigger('mouseleave', { lngLat, point });
};
// 是否包含某个点
hasPoint(pointId) {
return this._dataSource.findIndex(({ id }) => pointId === id) >= 0;
}
// 隐藏某个点
hidePoint(pointId) {
if (!this._map || !this.hasPoint(pointId)) return;
if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: false });
}
// 显示某个点
showPoint(pointId) {
if (!this._map || !this.hasPoint(pointId)) return;
if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: true });
}
changeVisibility(stateValue) {
if (!this._map) return;
if (this._map.getLayer(this._layerId)) {
this._map.setLayoutProperty(this._layerId, 'visibility', stateValue ? 'visible' : 'none');
}
}
clear() {
if (!this._map) return;
if (this._map.getSource(this._sourceId)) {
this._map.off('click', this._layerId, this._onClick);
this._map.off('mousemove', this._layerId, this._onMouseMove);
this._map.off('mouseleave', this._layerId, this._onMouseLeave);
this._map.off('mouseenter', this._layerId, this._onMouseEnter);
this._map.removeLayer(this._layerId);
this._map.removeSource(this._sourceId);
}
}
destroy() {
this.icons = {};
this._dataSource = [];
this.clear();
this._map = null;
}
}
export default IconPointRenderer;

138
src/core/Pinpoint.js

@ -0,0 +1,138 @@
/**
* 位置点
*/
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
const SOURCE_SYMBOL = 'pinpoint-source-symbol';
const LAYER_SYMBOL = 'pinpoint-layer-symbol';
class Pinpoint {
// 地图实例
map = null;
// 点坐标
coordinates = [];
// 操作点
pins = [];
// 视觉选项
option = {
mainColor: 'rgba(255,0,0,0.7)',
secondaryColor: '#fff',
shadowColor: 'rgba(255,0,0,0.2)',
fontSize: 12,
};
constructor(map, option = {}) {
this.map = map;
this.option = {
...this.option,
...option,
};
}
// 开启打点
onPinOn() {
this.map.getCanvas().style.cursor = 'crosshair';
this.coordinates = [];
this.pins = [];
this.map.on('click', this.onClickMap);
this.map.on('style.load', this.draw);
this.map.fire('pinpoint.on');
}
// 关闭打点
onPinOff() {
this.map.getCanvas().style.cursor = '';
if (this.map.getSource(SOURCE_SYMBOL)) {
this.map.removeLayer(LAYER_SYMBOL);
this.map.removeSource(SOURCE_SYMBOL);
}
this.pins.forEach(m => m.remove());
this.map.off('click', this.onClickMap);
this.map.off('style.load', this.draw);
this.map.fire('pinpoint.off');
}
// 生成点geojson
genPointFeature() {
const pointFeatures = this.coordinates.map(([lng, lat]) => turf.point([lng, lat], {
text: `${Number(lng).toFixed(7)}\n${Number(lat).toFixed(7)}`,
}));
return turf.featureCollection(pointFeatures);
}
draw = () => {
const sourceSymbol = this.map.getSource(SOURCE_SYMBOL);
if (!sourceSymbol) {
this.map.addSource(SOURCE_SYMBOL, {
type: 'geojson',
data: this.genPointFeature(),
});
this.map.addLayer({
id: LAYER_SYMBOL,
type: 'symbol',
source: SOURCE_SYMBOL,
layout: {
'text-field': '{text}',
'text-anchor': 'top',
'text-justify': 'right',
'text-size': this.option.fontSize,
'text-offset': [0, 0.8],
'text-allow-overlap': true,
},
paint: {
'text-color': this.option.mainColor,
'text-halo-color': this.option.secondaryColor,
'text-halo-width': 1,
},
});
} else {
sourceSymbol.setData(this.genPointFeature());
}
};
// 生成操作点
genPinNode() {
const node = document.createElement('div');
node.style.width = '17px';
node.style.height = '30px';
/* eslint-disable max-len */
node.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16.48 29.5">
<path d="M4.72,27.12c0,1.3,1.59,2.38,3.54,2.38s3.54-1,3.54-2.38-1.59-2.39-3.54-2.39S4.72,25.82,4.72,27.12Z" fill="${this.option.shadowColor}" />
<path d="M9.7.12A8.26,8.26,0,1,0,7.07,16.43V26a1.18,1.18,0,0,0,1.17,1.17h0A1.17,1.17,0,0,0,9.41,26V16.43a8.25,8.25,0,0,0,7.07-8.12h0A8.25,8.25,0,0,0,9.7.12ZM8.26,10.62A2.36,2.36,0,0,1,8.19,5.9h.07a2.36,2.36,0,0,1,0,4.72Z" fill="${this.option.mainColor}" />
</svg>`;
/* eslint-enable max-len */
return node;
}
onClickMap = e => {
const marker = new mapbox.Marker({
element: this.genPinNode(),
offset: [0, -12],
draggable: true,
}).setLngLat(e.lngLat).addTo(this.map);
const { lng, lat } = e.lngLat;
this.coordinates.push([lng, lat]);
this.draw();
this.pins.push(marker);
this.map.fire('pinpoint.change', { coordinates: this.coordinates });
marker.on('drag', () => {
const index = this.pins.indexOf(marker);
const { lng: newLng, lat: newLat } = marker.getLngLat();
this.coordinates[index] = [newLng, newLat];
this.draw();
});
marker.on('dragend', () => {
this.map.fire('pinpoint.change', { coordinates: this.coordinates });
});
};
}
export default Pinpoint;

185
src/core/Ruler.js

@ -0,0 +1,185 @@
/**
* 测距尺
*/
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
const SOURCE_LINE = 'ruler-source-line';
const SOURCE_SYMBOL = 'ruler-source-symbol';
const LAYER_LINE = 'ruler-layer-line';
const LAYER_SYMBOL = 'ruler-layer-symbol';
const labelFormat = n => (n < 1000 ? `${Number(n).toFixed(2)}` : `${(n / 1000).toFixed(2)}千米`);
class Ruler {
// 地图实例
map = null;
// 点坐标
coordinates = [];
// 距离值(含单位)
labels = [];
// 操作点
markers = [];
// 视觉选项
option = {
mainColor: 'rgba(255,0,0,0.8)',
secondaryColor: '#fff',
fontSize: 12,
};
constructor(map, option = {}) {
this.map = map;
this.option = {
...this.option,
...option,
};
}
// 开启测量(外部调用)
onMeasuringOn() {
this.map.getCanvas().style.cursor = 'crosshair';
this.coordinates = [];
this.markers = [];
this.labels = [];
this.map.on('click', this.onClickMap);
this.map.on('style.load', this.draw);
this.map.fire('ruler.on');
}
// 关闭测量(外部调用)
onMeasuringOff() {
this.map.getCanvas().style.cursor = '';
if (this.map.getSource(SOURCE_LINE)) {
this.map.removeLayer(LAYER_LINE);
this.map.removeSource(SOURCE_LINE);
}
if (this.map.getSource(SOURCE_SYMBOL)) {
this.map.removeLayer(LAYER_SYMBOL);
this.map.removeSource(SOURCE_SYMBOL);
}
this.markers.forEach(m => m.remove());
this.map.off('click', this.onClickMap);
this.map.off('style.load', this.draw);
this.map.fire('ruler.off');
}
// 生成线段geojson
genLineFeature() {
return turf.lineString(this.coordinates);
}
// 生成点geojson
genPointFeature() {
const pointFeatures = this.coordinates.map((coordinate, index) => turf.point(coordinate, {
text: this.labels[index],
}));
return turf.featureCollection(pointFeatures);
}
// 绘制
draw = () => {
if (this.coordinates.length >= 2) {
const sourceLine = this.map.getSource(SOURCE_LINE);
if (!sourceLine) {
this.map.addSource(SOURCE_LINE, {
type: 'geojson',
data: this.genLineFeature(),
});
this.map.addLayer({
id: LAYER_LINE,
type: 'line',
source: SOURCE_LINE,
paint: {
'line-color': this.option.mainColor,
'line-width': 2,
},
});
} else {
sourceLine.setData(this.genLineFeature());
}
}
const sourceSymbol = this.map.getSource(SOURCE_SYMBOL);
if (!sourceSymbol) {
this.map.addSource(SOURCE_SYMBOL, {
type: 'geojson',
data: this.genPointFeature(),
});
this.map.addLayer({
id: LAYER_SYMBOL,
type: 'symbol',
source: SOURCE_SYMBOL,
layout: {
'text-field': '{text}',
'text-anchor': 'top',
'text-size': this.option.fontSize,
'text-offset': [0, 0.8],
'text-allow-overlap': true,
},
paint: {
'text-color': this.option.mainColor,
'text-halo-color': this.option.secondaryColor,
'text-halo-width': 1,
},
});
} else {
sourceSymbol.setData(this.genPointFeature());
}
};
// 生成操作点
genMarkerNode() {
const node = document.createElement('div');
node.style.width = '12px';
node.style.height = '12px';
node.style.borderRadius = '50%';
node.style.background = this.option.secondaryColor;
node.style.boxSizing = 'border-box';
node.style.border = `2px solid ${this.option.mainColor}`;
return node;
}
onClickMap = e => {
const marker = new mapbox.Marker({
element: this.genMarkerNode(),
draggable: true,
}).setLngLat(e.lngLat).addTo(this.map);
const { lng, lat } = e.lngLat;
this.coordinates.push([lng, lat]);
this.updateLabels();
this.draw();
this.markers.push(marker);
this.map.fire('ruler.change', { coordinates: this.coordinates });
marker.on('drag', () => {
const index = this.markers.indexOf(marker);
const { lng: newLng, lat: newLat } = marker.getLngLat();
this.coordinates[index] = [newLng, newLat];
this.updateLabels();
this.draw();
});
marker.on('dragend', () => {
this.map.fire('ruler.change', { coordinates: this.coordinates });
});
};
// 更新测量结果值
updateLabels() {
const { coordinates } = this;
let sum = 0;
this.labels = coordinates.map((coordinate, index) => {
if (index === 0) return labelFormat(0);
sum += turf.distance(coordinates[index - 1], coordinates[index], { units: 'meters' });
return labelFormat(sum);
});
}
}
export default Ruler;

455
src/core/ShapeGroupRenderer.js

@ -0,0 +1,455 @@
/**
* 多边形组显示
*/
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
class ShapeGroupRenderer {
_topic = null;
// 地图实例
_map = null;
// 分组标识
_groupIdKey = null;
// 分组名称标识
_groupNameKey = null;
// 分组父级标识
_groupParentIdKey = null;
_dataSource = [];
_defaultOptions = {
// 显示描边
showStroke: true,
// 显示标签
showLabel: true,
// 显示填充
showFill: true,
};
_options = {};
_defaultStyle = {
color: 'rgba(255, 95, 0, 0.7)',
hoverColor: 'rgba(255, 95, 0, 1)',
offset: -2,
isDashed: false,
labelColor: 'rgba(255, 95, 0, 1)',
labelStrokeColor: 'rgba(0, 0, 0, 0.4)',
labelMinZoom: 3,
};
// 视觉选项
_style = {};
get _sourceId() {
return {
FILL: `${this._topic}-shape-fill-source`,
STROKE: `${this._topic}-shape-hull-stroke-source`,
LABEL: `${this._topic}-shape-hull-label-source`,
};
}
get _layerId() {
return {
FILL: `${this._topic}-shape-fill-source`,
STROKE: `${this._topic}-shape-hull-stroke-layer`,
LABEL: `${this._topic}-shape-hull-label-layer`,
};
}
// 形状填充
get _shapeFillFeatures() {
return this._dataSource.map(({ id, points, ...others }) => {
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
const lineFeature = turf.lineString(newPoints, { id, ...others });
const polygonFeature = turf.lineToPolygon(lineFeature);
polygonFeature.id = id;
return polygonFeature;
});
}
// 形状填充集合
get _shapeFillFeatureCollection() {
return turf.featureCollection(this._shapeFillFeatures);
}
// 形状填充色表达式
get _shapeFillColorExpression() {
return [
'case',
[
'any',
['boolean', ['feature-state', 'hover'], false],
['boolean', ['feature-state', 'highlight'], false],
],
this._options.showFill ? this._style.fillHoverColor : 'rgba(0, 0, 0, 0)',
this._options.showFill ? this._style.fillColor : 'rgba(0, 0, 0, 0)',
];
}
// 边框颜色表达式
get _strokeColorExpression() {
return [
'case',
[
'any',
['boolean', ['feature-state', 'hover'], false],
['boolean', ['feature-state', 'highlight'], false],
],
this._style.hoverColor,
this._style.color,
];
}
get _strokeDashedExpression() {
return this._style.isDashed ? [2, 2] : [1];
}
// 标签透明度表达式
get _labelOpacityExpression() {
return [
'interpolate', ['linear'],
['zoom'],
this._style.labelMinZoom - 0.01, 0,
this._style.labelMinZoom, 1,
];
}
// 分组数据
get _group() {
const group = {};
Object.values(this._dataSource).forEach(item => {
const { [this._groupIdKey]: groupId } = item;
if (groupId) {
group[groupId] = [
...(group[groupId] || []),
item,
];
}
});
return group;
}
// 每个组中所有形状边界点集合
get _pointFeatureCollections() {
const group = {};
Object.keys(this._group).forEach(groupId => {
const groupPoints = this._group[groupId].map(({ points }) => points).flat();
const pointFeatures = groupPoints.map(point => turf.point(point));
group[groupId] = turf.featureCollection(pointFeatures);
});
return group;
}
// 每个组的凸包
get _convexHulls() {
return Object.keys(this._pointFeatureCollections).map(groupId => {
const convexHull = turf.convex(this._pointFeatureCollections[groupId]);
const [{ [this._groupNameKey]: groupName, [this._groupParentIdKey]: groupParentId }] = this._group[groupId];
convexHull.id = groupId - 0;
convexHull.properties = {
[this._groupIdKey]: groupId - 0,
[this._groupNameKey]: groupName || '',
[this._groupParentIdKey]: (groupParentId - 0) || 0,
};
return convexHull;
});
}
// 组凸包集合
get _convexHullFeatureCollection() {
return turf.featureCollection(this._convexHulls);
}
// 各分组中心点
get _groupCenterFeatures() {
return this._convexHulls.map(feature => {
const pointFeature = turf.centerOfMass(feature);
pointFeature.properties = feature.properties;
return pointFeature;
});
}
// 分组中心点集合
get _groupCenterFeatureCollection() {
return turf.featureCollection(this._groupCenterFeatures);
}
constructor(topic, groupIdKey, groupNameKey = null, groupParentIdKey = null) {
this._topic = topic;
this._groupIdKey = groupIdKey;
this._groupNameKey = groupNameKey;
this._groupParentIdKey = groupParentIdKey;
this.updateOptions();
this.updateStyle();
makeObservable(this, {
_topic: observable,
_dataSource: observable,
_options: observable,
_style: observable,
_sourceId: computed,
_layerId: computed,
_strokeColorExpression: computed,
_strokeDashedExpression: computed,
_labelOpacityExpression: computed,
_group: computed,
_pointFeatureCollections: computed,
_convexHulls: computed,
_convexHullFeatureCollection: computed,
_groupCenterFeatures: computed,
_groupCenterFeatureCollection: computed,
loadDataSource: action,
updateOptions: action,
updateStyle: action,
destroy: action,
});
reaction(() => this._dataSource, () => {
this._render();
});
}
// 更新配置项
updateOptions(options = {}) {
if (Object.keys(options).length) {
this._options = {
...this._options,
...options,
};
this._refreshVisibility();
} else {
this._options = {
...this._defaultOptions,
};
}
}
// 更新视觉样式
updateStyle(style = {}) {
if (Object.keys(style).length) {
this._style = {
...this._style,
...style,
};
this._repaintStyle();
} else {
this._style = {
...this._defaultStyle,
};
}
}
setMap(mapInstance) {
if (this._map === mapInstance) return;
if (!(mapInstance instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this._map = mapInstance;
}
// 载入数据(list->item必须包含id、%_groupIdKey%、points数组,points->item如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
loadDataSource(list) {
if (!this._map) {
throw new Error('请先设置地图实例');
}
this._dataSource = (list || []).map(item => {
const { id, points } = item;
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
return (id >= 0 && Array.isArray(points) && points.length >= 3) ? { ...item, points: newPoints } : null;
}).filter(Boolean);
}
_render() {
if (!this._map) return;
if (this._options.showStroke) this._renderStroke();
if (this._options.showLabel) this._renderLabel();
if (this._options.showFill) this._renderShapeFill();
}
// 渲染形状填充
_renderShapeFill() {
const source = this._map.getSource(this._sourceId.FILL);
if (!source) {
this._map.addSource(this._sourceId.FILL, {
type: 'geojson',
data: this._shapeFillFeatureCollection,
});
this._map.addLayer({
id: this._layerId.FILL,
type: 'fill',
source: this._sourceId.FILL,
paint: {
'fill-color': this._shapeFillColorExpression,
'fill-opacity': [
'case',
['boolean', ['feature-state', 'visible'], true],
1,
0,
],
},
});
// this._map.on('click', this._layerId.FILL, this._onClick);
// this._map.on('mousemove', this._layerId.FILL, this._onMouseMove);
// this._map.on('mouseleave', this._layerId.FILL, this._onMouseLeave);
} else {
source.setData(this._shapeFillFeatureCollection);
}
}
_renderStroke() {
const source = this._map.getSource(this._sourceId.STROKE);
if (!source) {
this._map.addSource(this._sourceId.STROKE, {
type: 'geojson',
data: this._convexHullFeatureCollection,
});
this._map.addLayer({
id: this._layerId.STROKE,
type: 'line',
source: this._sourceId.STROKE,
paint: {
'line-color': this._strokeColorExpression,
'line-width': [
'case',
[
'any',
['boolean', ['feature-state', 'hover'], false],
['boolean', ['feature-state', 'highlight'], false],
],
2,
1,
],
'line-dasharray': this._strokeDashedExpression,
'line-offset': this._defaultStyle.offset,
},
});
} else {
source.setData(this._convexHullFeatureCollection);
}
}
_renderLabel() {
const source = this._map.getSource(this._sourceId.LABEL);
if (!source) {
this._map.addSource(this._sourceId.LABEL, {
type: 'geojson',
data: this._groupCenterFeatureCollection,
});
this._map.addLayer({
id: this._layerId.LABEL,
type: 'symbol',
source: this._sourceId.LABEL,
layout: {
'text-field': `{${this._groupNameKey}}`,
'text-size': 12,
'text-allow-overlap': true,
},
paint: {
'text-color': this._style.labelColor,
'text-halo-color': this._style.labelStrokeColor,
'text-halo-width': 1,
'text-translate': [0, -20],
'text-opacity': this._labelOpacityExpression,
},
});
} else {
source.setData(this._groupCenterFeatureCollection);
}
}
// 高亮某个分组框(或取消高亮)
highlightGroup(groupId, stateValue = true) {
if (!this._map) return;
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: groupId }, { highlight: stateValue });
if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id: groupId }, { highlight: stateValue });
}
// 高亮一组分组框(或取消高亮)
highlightGroups(keyName, keyValue, stateValue = true) {
if (!this._map) return;
turf.featureEach(this._convexHullFeatureCollection, currentFeature => {
const { id } = currentFeature;
if (currentFeature.properties[keyName] === keyValue) this.highlightGroup(id, stateValue);
});
}
_refreshVisibility() {
if (!this._map) return;
if (this._map.getLayer(this._layerId.STROKE)) this._map.setLayoutProperty(this._layerId.STROKE, 'visibility', this._options.showStroke ? 'visible' : 'none');
if (this._map.getLayer(this._layerId.LABEL)) this._map.setLayoutProperty(this._layerId.LABEL, 'visibility', this._options.showLabel ? 'visible' : 'none');
}
_repaintStyle() {
if (!this._map) return;
if (this._map.getLayer(this._layerId.FILL)) {
this._map.setPaintProperty(this._layerId.FILL, 'fill-color', this._shapeFillColorExpression);
}
if (this._map.getLayer(this._layerId.STROKE)) {
this._map.setPaintProperty(this._layerId.STROKE, 'line-color', this._strokeColorExpression);
this._map.setPaintProperty(this._layerId.STROKE, 'line-dasharray', this._strokeDashedExpression);
this._map.setPaintProperty(this._layerId.STROKE, 'line-offset', this._defaultStyle.offset);
}
if (this._map.getLayer(this._layerId.LABEL)) {
this._map.setPaintProperty(this._layerId.LABEL, 'text-color', this._style.labelColor);
this._map.setPaintProperty(this._layerId.LABEL, 'text-halo-color', this._style.labelStrokeColor);
this._map.setPaintProperty(this._layerId.LABEL, 'text-opacity', this._labelOpacityExpression);
}
}
// 缩放到包围盒
fit(groupId, { top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
if (!this._map || !(groupId in this._group)) return;
const feature = this._convexHullFeatureCollection.features.find(({ id }) => id === groupId);
if (!feature) return;
this._map.fitBounds(turf.bbox(feature), {
duration: 2000,
padding: {
top: top + cut,
bottom: bottom + cut,
left: left + cut,
right: right + cut,
},
});
}
clear() {
if (!this._map) return;
if (this._map.getSource(this._sourceId.STROKE)) {
this._map.removeLayer(this._layerId.STROKE);
this._map.removeSource(this._sourceId.STROKE);
}
if (this._map.getSource(this._sourceId.LABEL)) {
this._map.removeLayer(this._layerId.LABEL);
this._map.removeSource(this._sourceId.LABEL);
}
if (this._map.getSource(this._sourceId.FILL)) {
// this._map.off('click', this._layerId.FILL, this._onClick);
// this._map.off('mousemove', this._layerId.FILL, this._onMouseMove);
// this._map.off('mouseleave', this._layerId.FILL, this._onMouseLeave);
this._map.removeLayer(this._layerId.FILL);
this._map.removeSource(this._sourceId.FILL);
}
}
destroy() {
this.clear();
this._map = null;
this._dataSource = [];
this.updateStyle();
this.updateOptions();
}
}
export default ShapeGroupRenderer;

63
src/core/blockHelper/BlockHelper.js

@ -0,0 +1,63 @@
/**
* 地块助手
*/
import blockRenderer from './blockRenderer';
import groupRenderer from './groupRenderer';
import subFarmRenderer from './subFarmRenderer';
class BlockHelper {
static blockRenderer = blockRenderer;
static groupRenderer = groupRenderer;
static subFarmRenderer = subFarmRenderer;
static setMap(map) {
blockRenderer.setMap(map);
groupRenderer.setMap(map);
subFarmRenderer.setMap(map);
}
static loadDataSource(list = []) {
blockRenderer.loadDataSource(list);
groupRenderer.loadDataSource(list);
subFarmRenderer.loadDataSource(list);
}
static getBlock(blockId) {
return blockRenderer.getShape(blockId - 0);
}
static highlightBlock(blockId, stateValue) {
blockRenderer.highlightShape(blockId - 0, stateValue);
}
static highlightGroup(groupId, stateValue) {
groupRenderer.highlightGroup(groupId - 0, stateValue);
blockRenderer.highlightShapes('groupId', groupId - 0, stateValue);
}
static highlightSubFarm(subFarmId, stateValue) {
subFarmRenderer.highlightGroup(subFarmId - 0, stateValue);
groupRenderer.highlightGroups('subFarmId', subFarmId - 0, stateValue);
blockRenderer.highlightShapes('subFarmId', subFarmId - 0, stateValue);
}
static fitBlock(blockId, padding = {}, cut = 120) {
blockRenderer.fitShape(blockId - 0, padding, cut);
}
static fitGroup(groupId, padding = {}, cut = 120) {
groupRenderer.fit(groupId - 0, padding, cut);
}
static fitSubFarm(subFarmId, padding = {}, cut = 120) {
subFarmRenderer.fit(subFarmId - 0, padding, cut);
}
static fitView(padding = {}, cut = 120) {
blockRenderer.fitView(padding, cut);
}
}
export default BlockHelper;

23
src/core/blockHelper/blockRenderer.js

@ -0,0 +1,23 @@
/**
* 地块显示
*/
import ShapeRenderer from '../ShapeRenderer';
class BlockRenderer extends ShapeRenderer {
_defaultStyle = {
fillColor: 'rgba(255, 255, 0, 0.1)',
fillHoverColor: 'rgba(255, 255, 0, 0.3)',
strokeColor: 'rgba(255, 255, 0, 0.5)',
strokeHoverColor: 'rgba(255, 255, 0, 1)',
labelColor: 'rgba(0, 0, 0, 1)',
labelStrokeColor: 'rgba(255, 255, 255, 1)',
labelMinZoom: 16,
};
constructor() {
super('block');
this.updateStyle();
}
}
export default new BlockRenderer();

23
src/core/blockHelper/groupRenderer.js

@ -0,0 +1,23 @@
/**
* 地块分组显示
*/
import ShapeGroupRenderer from '../ShapeGroupRenderer';
class GroupRenderer extends ShapeGroupRenderer {
_defaultStyle = {
color: 'rgba(255, 95, 0, 0.7)',
hoverColor: 'rgba(255, 95, 0, 1)',
offset: -2,
isDashed: true,
labelColor: 'rgba(255, 95, 0, 1)',
labelStrokeColor: 'rgba(0, 0, 0, 0.4)',
labelMinZoom: 14,
};
constructor() {
super('blockGroup', 'groupId', 'groupName', 'subFarmId');
this.updateStyle();
}
}
export default new GroupRenderer();

26
src/core/blockHelper/subFarmRenderer.js

@ -0,0 +1,26 @@
/**
* 地块分组显示
*/
import ShapeGroupRenderer from '../ShapeGroupRenderer';
class SubFarmRenderer extends ShapeGroupRenderer {
_defaultOptions = {
showStroke: true,
showLabel: false,
};
_defaultStyle = {
color: 'rgba(170, 0, 255, 0.7)',
hoverColor: 'rgba(170, 0, 255, 1)',
offset: -5,
isDashed: false,
};
constructor() {
super('blockSubFarm', 'subFarmId', 'subFarmName', 'farmId');
this.updateOptions();
this.updateStyle();
}
}
export default new SubFarmRenderer();

356
src/core/clusterControlHelper.js

@ -0,0 +1,356 @@
/**
* 集群控制助手
* 分段显示地块边+选择边+显示航线轨迹
*/
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
import chroma from 'chroma-js';
const SOURCE_CLUSTER_CONTROL_EDGE_LINE = 'cluster-control-edge-line-source';
const LAYER_CLUSTER_CONTROL_EDGE_LINE = 'cluster-control-edge-line-layer';
const SOURCE_CLUSTER_CONTROL_EDGE_LABEL = 'cluster-control-edge-label-source';
const LAYER_CLUSTER_CONTROL_EDGE_LABEL = 'cluster-control-edge-label-layer';
const SOURCE_CLUSTER_CONTROL_TRACK_LINE = 'cluster-control-track-line-source';
const LAYER_CLUSTER_CONTROL_TRACK_LINE = 'cluster-control-track-line-layer';
const SOURCE_CLUSTER_CONTROL_TRACK_LABEL = 'cluster-control-track-label-source';
const LAYER_CLUSTER_CONTROL_TRACK_LABEL = 'cluster-control-track-label-layer';
class ClusterControlHelper {
// 地图实例
map = null;
// 单个地块数据
dataSource = {};
// 多条轨迹
tracks = [];
// 鼠标经过的边索引
hoveredEdgeIndex = null;
// 选边回调
pickEdgeCallback = () => {};
// 地块各边集合
get edgeLineFeatureCollection() {
const { points } = this.dataSource;
if (!Array.isArray(points) || !points.length) {
return turf.featureCollection([]);
}
const boundaryFeature = turf.lineString(points);
return turf.lineSegment(turf.lineToPolygon(boundaryFeature));
}
// 每条边中点
get edgeLineMiddlePointFeatureCollection() {
const points = [];
turf.featureEach(this.edgeLineFeatureCollection, (feature, index) => {
const [point1, point2] = turf.getCoords(feature).map(position => turf.point(position));
const point = turf.midpoint(point1, point2);
point.id = index;
point.properties = { n: index + 1 };
points.push(point);
});
return turf.featureCollection(points);
}
// 航线颜色
get trackColors() {
return chroma.scale([
chroma.hsl(0, 1, 0.4),
chroma.hsl(30, 1, 0.4),
chroma.hsl(60, 1, 0.4),
chroma.hsl(90, 1, 0.4),
chroma.hsl(120, 1, 0.4),
chroma.hsl(150, 1, 0.4),
chroma.hsl(180, 1, 0.4),
chroma.hsl(210, 1, 0.4),
chroma.hsl(240, 1, 0.4),
chroma.hsl(270, 1, 0.4),
chroma.hsl(300, 1, 0.4),
chroma.hsl(330, 1, 0.4),
chroma.hsl(360, 1, 0.4),
]).colors(this.tracks.length + 1);
}
// 航线
get trackLineFeatureCollection() {
const features = this.tracks.map((track, index) => {
const color = this.trackColors[index];
return turf.lineString(track, { color }, { id: index });
});
return turf.featureCollection(features);
}
// 航线编号
get trackLabelFeatureCollection() {
const showNumber = this.tracks.length > 1;
const features = this.tracks.map((track, index) => {
const color = this.trackColors[index];
const number = showNumber ? index + 1 : '';
const [point] = track;
return turf.point(point, { color, number }, { id: index });
});
return turf.featureCollection(features);
}
constructor() {
makeObservable(this, {
dataSource: observable,
tracks: observable,
edgeLineFeatureCollection: computed,
edgeLineMiddlePointFeatureCollection: computed,
trackColors: computed,
trackLineFeatureCollection: computed,
trackLabelFeatureCollection: computed,
loadBlock: action,
loadTracks: action,
destroy: action,
});
reaction(() => this.dataSource, () => {
this.render();
});
reaction(() => this.tracks, () => {
this.renderTracks();
});
}
// 设置地图实例
setMap(map) {
if (this.map === map) return;
if (!(map instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this.map = map;
}
// 载入地块详情
loadBlock(detail) {
if (!this.map) {
throw new Error('请先设置地图实例');
}
const { points, ...otherDetail } = detail;
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
this.dataSource = {
...otherDetail,
points: newPoints,
};
}
// 载入多条轨迹
loadTracks(tracks) {
this.tracks = (tracks || []).map(track => (track || []).map(({ longitude, latitude }) => [longitude, latitude]));
}
render() {
if (!this.map) return;
this.renderEdgeLine();
this.renderEdgeLabel();
}
// 边界线
renderEdgeLine() {
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE);
if (!source) {
this.map.addSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE, {
type: 'geojson',
data: this.edgeLineFeatureCollection,
});
this.map.addLayer({
id: LAYER_CLUSTER_CONTROL_EDGE_LINE,
type: 'line',
source: SOURCE_CLUSTER_CONTROL_EDGE_LINE,
layout: {
'line-cap': 'round',
},
paint: {
'line-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'rgba(0,0,0,1)',
'rgba(0,0,0,0.5)',
],
'line-width': 6,
},
});
this.map.on('mouseenter', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onEnterEdge);
this.map.on('mouseleave', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onLeaveEdge);
this.map.on('click', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onPickEdge);
} else {
source.setData(this.edgeLineFeatureCollection);
}
}
renderEdgeLabel() {
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL);
if (!source) {
this.map.addSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL, {
type: 'geojson',
data: this.edgeLineMiddlePointFeatureCollection,
});
this.map.addLayer({
id: LAYER_CLUSTER_CONTROL_EDGE_LABEL,
type: 'symbol',
source: SOURCE_CLUSTER_CONTROL_EDGE_LABEL,
layout: {
'text-field': '{n}',
'text-size': 16,
'text-allow-overlap': true,
},
paint: {
'text-color': 'white',
'text-halo-color': 'black',
'text-halo-width': 2,
},
});
} else {
source.setData(this.edgeLineMiddlePointFeatureCollection);
}
}
renderTracks() {
if (!this.map) return;
// 轨迹线
{
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE);
if (!source) {
this.map.addSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE, {
type: 'geojson',
data: this.trackLineFeatureCollection,
});
this.map.addLayer({
id: LAYER_CLUSTER_CONTROL_TRACK_LINE,
type: 'line',
source: SOURCE_CLUSTER_CONTROL_TRACK_LINE,
layout: {
'line-cap': 'round',
},
paint: {
'line-color': ['get', 'color'],
'line-width': 2,
},
});
} else {
this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE).setData(this.trackLineFeatureCollection);
this.fadeTracks(1);
}
}
// 轨迹编号
{
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL);
if (!source) {
this.map.addSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL, {
type: 'geojson',
data: this.trackLabelFeatureCollection,
});
this.map.addLayer({
id: LAYER_CLUSTER_CONTROL_TRACK_LABEL,
type: 'symbol',
source: SOURCE_CLUSTER_CONTROL_TRACK_LABEL,
layout: {
'text-field': ['get', 'number'],
'text-allow-overlap': true,
},
paint: {
'text-color': ['get', 'color'],
'text-halo-width': 1,
'text-halo-color': 'black',
},
});
} else {
this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL).setData(this.trackLabelFeatureCollection);
}
}
}
// 淡化
fadeTracks(opacity) {
if (!this.map) return;
if (this.map.getLayer(LAYER_CLUSTER_CONTROL_TRACK_LINE)) {
this.map.setPaintProperty(LAYER_CLUSTER_CONTROL_TRACK_LINE, 'line-opacity', opacity);
}
}
onEnterEdge = e => {
if (!e.features.length) return;
this.map.getCanvas().style.cursor = 'pointer';
if (this.hoveredEdgeIndex !== null) {
this.map.setFeatureState(
{ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex },
{ hover: false },
);
}
this.hoveredEdgeIndex = e.features[0].id;
this.map.setFeatureState(
{ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex },
{ hover: true },
);
};
onLeaveEdge = () => {
this.map.getCanvas().style.cursor = '';
if (this.hoveredEdgeIndex !== null) {
this.map.setFeatureState(
{ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex },
{ hover: false },
);
}
this.hoveredEdgeIndex = null;
};
onPickEdge = e => {
const [{ id } = {}] = e.features;
this.pickEdgeCallback(id);
};
// 清除边
clearEdge() {
if (!this.map) return;
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE)) {
this.map.off('mouseenter', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onEnterEdge);
this.map.off('mouseleave', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onLeaveEdge);
this.map.off('click', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onPickEdge);
this.map.removeLayer(LAYER_CLUSTER_CONTROL_EDGE_LINE);
this.map.removeSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE);
}
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL)) {
this.map.removeLayer(LAYER_CLUSTER_CONTROL_EDGE_LABEL);
this.map.removeSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL);
}
}
// 清除航线
clearTrack() {
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE)) {
this.map.removeLayer(LAYER_CLUSTER_CONTROL_TRACK_LINE);
this.map.removeSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE);
}
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL)) {
this.map.removeLayer(LAYER_CLUSTER_CONTROL_TRACK_LABEL);
this.map.removeSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL);
}
}
clearAll() {
this.clearEdge();
this.clearTrack();
}
destroy() {
this.clearAll();
this.pickEdgeCallback = () => {};
this.hoveredEdgeIndex = null;
this.tracks = [];
this.dataSource = {};
this.map = null;
}
}
export default new ClusterControlHelper();

308
src/core/deviceCruise.js

@ -0,0 +1,308 @@
/**
* 设备按轨迹巡航
*/
// import Vue from 'vue';
import { reactive } from 'vue';
import { interpolate } from 'popmotion';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
import chroma from 'chroma-js';
import Icon from './IconPoint/Icon';
import DroneIcon from '../assets/DroneIcon';
const SOURCE_DEVICE_CRUISE_POINT = 'device-cruise-point-source';
const LAYER_DEVICE_CRUISE_POINT = 'device-cruise-point-layer';
const DEVICE_CRUISE_POINT_ICON = 'device-cruise-point-icon';
class DeviceCruise {
_that = null;
// 地图实例
_map = null;
_icon = null;
// 单条轨迹数据
_dataSource = {};
// 巡航速率
speedRate = 1;
// 动画计时器
_timer = null;
// 上一帧时间点
_lastFrameAt = 0;
// 累计播放时长(毫秒数)
elapsedMs = 0;
// 每帧期望间隔(毫秒数,实际间隔取决于浏览器fps)
_fpsInterval = 1000 / 30;
// 上一帧时间戳
_lastFrameTimestamp = 0;
// 巡航到的时间点数据
timelyData = {};
isPlaying = false;
isPaused = false;
isStopped = true;
// 视觉样式
_defaultStyle = {
deviceMainColor: '#1890ff',
deviceDirectionColor: 'rgba(255,255,0,0.75)',
};
_style = {};
// 是否准备完毕
get ready() {
return Object.keys(this._that._dataSource).length > 0;
}
get _points() {
const { points } = this._that._dataSource;
return points || [];
}
// 基准时间点(从0开始的毫秒数)
get _datumTime() {
const [{ timestamp: startTs } = {}] = this._that._points || [];
return this._that._points.map(({ timestamp }) => timestamp - startTs);
}
// 轨迹总时间(毫秒数)
get totalTime() {
return this._that._datumTime[this._that._datumTime.length - 1] || 0;
}
// 是否已经开始播放了
get isStarted() {
return this._that.isPlaying || this._that.isPaused;
}
constructor() {
this._that = reactive(this);
// Vue.observable(this);
this._that.initStyle();
// eslint-disable-next-line no-constructor-return
return this._that;
}
// 初始化视觉演示(需在loadTracks之前配置)
initStyle(style = {}) {
this._that._style = {
...this._that._defaultStyle,
...style,
};
}
// 设置地图实例
setMap(mapInstance) {
if (this._that._map === mapInstance) return;
if (!(mapInstance instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this._that._map = mapInstance;
}
setIcon(icon) {
if (!(icon instanceof Icon)) {
throw new Error(`入参icon必须是${Icon}的实例`);
}
this._that._icon = icon;
if (this._that._map.hasImage(this._that._icon.name)) {
this._that._map.updateImage(this._that._icon.name, this._that._icon.data);
} else {
this._that._map.addImage(this._that._icon.name, this._that._icon.data);
}
}
// 载入轨迹数据(point中必须包含lng, lat, timestamp, yaw)
loadTrack({ id, points, ...others }) {
if (!this._that._map) {
throw new Error('请先设置地图实例');
}
this._that._dataSource = {
id,
points: (points || []).map(item => ({ ...item })),
...others,
};
this._that._reset();
this._that._initDevice();
}
// 设置速率
setSpeedRate(val) {
this._that.speedRate = val;
}
// 设置当前巡航时间点
setCurrentTime(ms) {
if (ms < 0 || ms > this._that.totalTime) return;
this._that.elapsedMs = ms;
// if (!this._that.isStarted) {
this._that._renderDevice();
// }
}
// 获取指定毫秒处的数据值
_getTimelyData(ms = 0) {
const genTimelyData = interpolate(this._that._datumTime, this._that._points);
return genTimelyData(ms);
}
_reset() {
this._that.isPlaying = false;
this._that.isPaused = false;
this._that.isStopped = true;
this._that.speedRate = 1;
this._that._lastFrameAt = 0;
this._that.elapsedMs = 0;
this._that._lastFrameTimestamp = 0;
}
_initDevice() {
if (!this._that._icon) {
const icon = new Icon(DEVICE_CRUISE_POINT_ICON, new DroneIcon({
outlineColor: this._that._style.deviceMainColor,
bodyColor: chroma(this._that._style.deviceMainColor).alpha(0.5).css(),
directionColor: this._that._style.deviceDirectionColor,
}), 0.25);
this._that.setIcon(icon);
}
const [point] = this._that._points;
const { lng, lat, yaw } = point || {};
const pointFeature = point ? turf.point([lng, lat], {
...point,
yaw: (+Number(yaw) || 0).toFixed(2),
}) : turf.multiPoint([]);
const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT);
if (!source) {
this._that._map.addSource(SOURCE_DEVICE_CRUISE_POINT, {
type: 'geojson',
data: pointFeature,
});
this._that._map.addLayer({
id: LAYER_DEVICE_CRUISE_POINT,
type: 'symbol',
source: SOURCE_DEVICE_CRUISE_POINT,
layout: {
'icon-image': this._that._icon.name,
'icon-size': this._that._icon.zoom,
'icon-offset': this._that._icon.offset,
'icon-rotate': ['get', 'yaw'],
'icon-allow-overlap': true,
},
});
} else {
source.setData(pointFeature);
this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-image', this._that._icon.name);
this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-size', this._that._icon.zoom);
this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-offset', this._that._icon.offset);
}
}
_clearDevice() {
if (!this._that._map) return;
if (this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT)) {
this._that._map.removeLayer(LAYER_DEVICE_CRUISE_POINT);
this._that._map.removeSource(SOURCE_DEVICE_CRUISE_POINT);
}
}
_renderDevice() {
if (!this._that._points.length) {
const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT);
if (source) source.setData(turf.featureCollection([]));
return;
}
const point = this._that._getTimelyData(this._that.elapsedMs);
this._that.timelyData = point;
if (!point) return;
const { lng, lat, yaw } = point;
const pointFeature = turf.point([lng, lat], {
...point,
yaw: (+Number(yaw) || 0).toFixed(2),
});
const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT);
source.setData(pointFeature);
}
_ticker = timestamp => {
this._that.elapsedMs += Math.round((timestamp - this._that._lastFrameAt) * this._that.speedRate);
this._that._lastFrameAt = timestamp;
if (this._that.elapsedMs > this._that.totalTime) {
return;
}
// 在期望的间隔内_renderDevice,而不是每个tick都_renderDevice(目的:降低render频率,提高显示性能)
const now = Date.now();
const timeDiff = now - this._that._lastFrameTimestamp;
if (timeDiff > this._that._fpsInterval) {
this._that._lastFrameTimestamp = now - (timeDiff % this._that._fpsInterval); // 矫正时间戳
this._that._renderDevice();
}
this._that._timer = requestAnimationFrame(this._that._ticker);
};
// 开始播放巡航动画、恢复播放巡航动画
handlePlay() {
this._that.isPlaying = true;
this._that.isPaused = false;
this._that.isStopped = false;
this._that._lastFrameAt = performance.now();
requestAnimationFrame(this._that._ticker);
}
// 暂停播放巡航动画
handlePause() {
this._that.isPlaying = false;
this._that.isPaused = true;
this._that.isStopped = false;
cancelAnimationFrame(this._that._timer);
}
// 停止播放巡航动画
handleStop() {
this._that.isPlaying = false;
this._that.isPaused = false;
this._that.isStopped = true;
this._that._lastFrameAt = 0;
this._that.elapsedMs = 0;
cancelAnimationFrame(this._that._timer);
this._that._renderDevice();
this._that.timelyData = {};
}
clear() {
if (!this._that._map) return;
this._that.handleStop();
this._that._clearDevice();
this._that._reset();
this._that._dataSource = {};
this._that.timelyData = {};
}
destroy() {
this._that.clear();
this._that._timer = null;
this._that._map = null;
this._that.initStyle();
}
}
export default new DeviceCruise();

17
src/core/obstacleHelper/ObstacleHelper.js

@ -0,0 +1,17 @@
/**
* 障碍物助手
*/
import obstacleRenderer from './obstacleRenderer';
import blockObstacleRenderer from './blockObstacleRenderer';
class ObstacleHelper {
static obstacleRenderer = obstacleRenderer;
static blockObstacleRenderer = blockObstacleRenderer;
static setMap(map) {
obstacleRenderer.setMap(map);
}
}
export default ObstacleHelper;

31
src/core/obstacleHelper/blockObstacleRenderer.js

@ -0,0 +1,31 @@
/**
* 地块障碍物
*/
import ShapeRenderer from '../ShapeRenderer';
class BlockObstacleRenderer extends ShapeRenderer {
_defaultOptions = {
showFill: true,
showStroke: false,
showLabel: true,
showLabelOnOver: true,
};
_defaultStyle = {
fillColor: 'rgba(255, 0, 0, 0.2)',
fillHoverColor: 'rgba(255, 0, 0, 0.8)',
strokeColor: 'rgba(255, 0, 0, 0.5)',
strokeHoverColor: 'rgba(255, 0, 0, 1)',
labelColor: 'rgba(0, 0, 0, 1)',
labelStrokeColor: 'rgba(255, 255, 255, 1)',
labelMinZoom: 3,
};
constructor() {
super('block-obstacle');
this.updateOptions();
this.updateStyle();
}
}
export default new BlockObstacleRenderer();

31
src/core/obstacleHelper/obstacleRenderer.js

@ -0,0 +1,31 @@
/**
* 所有障碍物
*/
import ShapeRenderer from '../ShapeRenderer';
class ObstacleRenderer extends ShapeRenderer {
_defaultOptions = {
showFill: true,
showStroke: false,
showLabel: true,
showLabelOnOver: true,
};
_defaultStyle = {
fillColor: 'rgba(255, 0, 0, 0.2)',
fillHoverColor: 'rgba(255, 0, 0, 0.8)',
strokeColor: 'rgba(255, 0, 0, 0.5)',
strokeHoverColor: 'rgba(255, 0, 0, 1)',
labelColor: 'rgba(0, 0, 0, 1)',
labelStrokeColor: 'rgba(255, 255, 255, 1)',
labelMinZoom: 3,
};
constructor() {
super('obstacle');
this.updateOptions();
this.updateStyle();
}
}
export default new ObstacleRenderer();

443
src/core/trackRenderer.js

@ -0,0 +1,443 @@
/**
* 多轨迹在地图上显示
*/
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
import EventDispatcher from './EventDispatcher';
const SOURCE_RUNNING_TRACK_LINE = 'running-track-line-source';
const LAYER_RUNNING_TRACK_LINE = 'running-track-line-layer';
const SOURCE_WORKING_TRACK_LINE = 'running-working-line-source';
const LAYER_WORKING_TRACK_LINE = 'running-working-line-layer';
const SOURCE_START_TRACK_POINT = 'start-track-point-source';
const LAYER_START_TRACK_POINT = 'start-track-point-layer';
const SOURCE_END_TRACK_POINT = 'end-track-point-source';
const LAYER_END_TRACK_POINT = 'end-track-point-layer';
class TrackRenderer extends EventDispatcher {
// 地图实例
_map = null;
// 轨迹容器
_tracksStore = [];
// 每条轨迹唯一键
_trackKey = 'id';
// 鼠标经过的轨迹id
_hoveredId = null;
// 鼠标经过显示的气泡框
_popup = new mapbox.Popup({ closeButton: false });
// 以回调形式获取popup显示内容
_getPopupHtml = () => 'Nothing';
// 显示配置项
_defaultOptions = {
// 最小显示级别
minZoom: 3,
// 显示端点
showEndpoint: true,
// 显示作业轨迹
showWorkingTrack: true,
// 显示行驶轨迹
showRunningTrack: true,
// 显示气泡框
showPopup: false,
};
_options = {};
// 视觉样式
_defaultStyle = {
startPointColor: 'rgba(0,0,255,0.8)',
endPointColor: 'rgba(0,255,0,0.8)',
pointStrokeColor: '#ffffff',
runningTrackColor: 'rgba(255,255,255,0.3)',
runningTrackHoverColor: 'rgba(255,255,255,0.8)',
workingTrackColor: 'rgba(255,0,0,0.3)',
workingTrackHoverColor: 'rgba(255,0,0,0.6)',
};
_style = {};
// 所有轨迹起始点
get _startPointsFeature() {
if (!this._options.showEndpoint) return turf.featureCollection([]);
const positions = this._tracksStore.map(({ points }) => {
const [{ lng, lat }] = points;
return [lng, lat];
});
return turf.multiPoint(positions);
}
// 所有轨迹结束点
get _endPointsFeature() {
if (!this._options.showEndpoint) return turf.featureCollection([]);
const positions = this._tracksStore.map(({ points }) => {
const { lng, lat } = points[points.length - 1];
return [lng, lat];
});
return turf.multiPoint(positions);
}
// 行驶轨迹集合
get _runningTrackFeatureCollection() {
if (!this._options.showRunningTrack) return turf.featureCollection([]);
const features = this._tracksStore.map(detail => {
const { points, [this._trackKey]: id, ...others } = detail;
const positions = points.map(({ lng, lat }) => ([lng, lat]));
return turf.lineString(positions, { id, ...others }, { id });
});
return turf.featureCollection(features);
}
// 作业轨迹集合
get _workingTrackFeatureCollection() {
if (!this._options.showWorkingTrack) return turf.featureCollection([]);
const features = this._tracksStore.map(({ points, [this._trackKey]: id }) => {
const segments = TrackRenderer.pickWorkingTrackSegments(points);
return turf.multiLineString(segments, {}, { id });
});
return turf.featureCollection(features);
}
// 限位框
get _boundingBox() {
return turf.bbox(this._runningTrackFeatureCollection);
}
// 拾取某个轨迹里的各个作业片段
static pickWorkingTrackSegments(trackPoints) {
const result = [];
let segment = [];
trackPoints.forEach(({ lng, lat, flowSpeed }) => {
if (flowSpeed > 0) {
segment.push([lng, lat]);
} else {
if (segment.length >= 2) {
result.push(segment);
}
segment = [];
}
});
return result;
}
constructor() {
super(['rendered']);
this.initOptions();
this.initStyle();
makeObservable(this, {
_tracksStore: observable,
_startPointsFeature: computed,
_endPointsFeature: computed,
_runningTrackFeatureCollection: computed,
_workingTrackFeatureCollection: computed,
loadTracks: action,
destroy: action,
});
reaction(() => this._tracksStore, () => {
if (this._tracksStore.length) {
this._render();
} else {
this._clear();
}
});
}
// 初始化配置(需在loadTracks之前配置)
initOptions(options = {}) {
this._options = {
...this._defaultOptions,
...options,
};
}
// 初始化视觉演示(需在loadTracks之前配置)
initStyle(style = {}) {
this._style = {
...this._defaultStyle,
...style,
};
}
// 设置地图实例
setMap(mapInstance) {
if (this._map === mapInstance) return;
if (!(mapInstance instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this._map = mapInstance;
}
// 载入轨迹数据
loadTracks(list, trackKey = 'id') {
if (!this._map) {
throw new Error('请先设置地图实例');
}
// list = [{ id: 123, points: [{ lng, lat, yaw, flowSpeed }] }];
this._tracksStore = (list || []).slice();
this._trackKey = trackKey;
}
// 设置气泡框显示内容回调
setPopupContentCallback(callback) {
if (typeof callback !== 'function') {
throw new Error('入参必须是一个函数');
}
this._getPopupHtml = callback;
}
_render() {
if (this._options.showRunningTrack) this._renderRunningTrack();
if (this._options.showWorkingTrack) this._renderWorkingTrack();
if (this._options.showRunningTrack) {
this._renderStartPoint();
this._renderEndPoint();
}
this._trigger('rendered');
}
_renderStartPoint() {
const source = this._map.getSource(SOURCE_START_TRACK_POINT);
if (!source) {
this._map.addSource(SOURCE_START_TRACK_POINT, {
type: 'geojson',
data: this._startPointsFeature,
});
this._map.addLayer({
id: LAYER_START_TRACK_POINT,
type: 'circle',
source: SOURCE_START_TRACK_POINT,
paint: {
'circle-radius': 4,
'circle-color': this._style.startPointColor,
'circle-stroke-width': 1,
'circle-stroke-color': this._style.pointStrokeColor,
'circle-stroke-opacity': 0.6,
'circle-opacity': [
'interpolate', ['linear'],
['zoom'],
this._options.minZoom - 0.01, 0,
this._options.minZoom, 1,
],
},
});
} else {
source.setData(this._startPointsFeature);
}
}
_renderEndPoint() {
const source = this._map.getSource(SOURCE_END_TRACK_POINT);
if (!source) {
this._map.addSource(SOURCE_END_TRACK_POINT, {
type: 'geojson',
data: this._endPointsFeature,
});
this._map.addLayer({
id: LAYER_END_TRACK_POINT,
type: 'circle',
source: SOURCE_END_TRACK_POINT,
paint: {
'circle-radius': 4,
'circle-color': this._style.endPointColor,
'circle-stroke-width': 1,
'circle-stroke-color': this._style.pointStrokeColor,
'circle-stroke-opacity': 0.6,
'circle-opacity': [
'interpolate', ['linear'],
['zoom'],
this._options.minZoom - 0.01, 0,
this._options.minZoom, 1,
],
},
});
} else {
source.setData(this._endPointsFeature);
}
}
_renderRunningTrack() {
const source = this._map.getSource(SOURCE_RUNNING_TRACK_LINE);
if (!source) {
this._map.addSource(SOURCE_RUNNING_TRACK_LINE, {
type: 'geojson',
data: this._runningTrackFeatureCollection,
});
this._map.addLayer({
id: LAYER_RUNNING_TRACK_LINE,
type: 'line',
source: SOURCE_RUNNING_TRACK_LINE,
paint: {
'line-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
this._style.runningTrackHoverColor,
this._style.runningTrackColor,
],
'line-width': 3,
'line-opacity': [
'interpolate', ['linear'],
['zoom'],
this._options.minZoom - 0.01, 0,
this._options.minZoom, 1,
],
},
layout: {
'line-cap': 'round',
'line-join': 'round',
},
});
this._map.on('mousemove', LAYER_RUNNING_TRACK_LINE, this._onMouseMove);
this._map.on('mouseleave', LAYER_RUNNING_TRACK_LINE, this._onMouseLeave);
this._map.on('click', LAYER_RUNNING_TRACK_LINE, this._onClick);
} else {
source.setData(this._runningTrackFeatureCollection);
}
}
_renderWorkingTrack() {
const source = this._map.getSource(SOURCE_WORKING_TRACK_LINE);
if (!source) {
this._map.addSource(SOURCE_WORKING_TRACK_LINE, {
type: 'geojson',
data: this._workingTrackFeatureCollection,
});
this._map.addLayer({
id: LAYER_WORKING_TRACK_LINE,
type: 'line',
source: SOURCE_WORKING_TRACK_LINE,
paint: {
'line-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
this._style.workingTrackHoverColor,
this._style.workingTrackColor,
],
'line-width': 6,
'line-opacity': [
'interpolate', ['linear'],
['zoom'],
this._options.minZoom - 0.01, 0,
this._options.minZoom, 1,
],
},
layout: {
'line-cap': 'round',
'line-join': 'round',
},
});
} else {
source.setData(this._workingTrackFeatureCollection);
}
}
_onMouseMove = e => {
if (this._map.getZoom() < this._options.minZoom) return;
this._map.getCanvas().style.cursor = 'pointer';
if (e.features.length > 0) {
if (this._hoveredId !== null) {
const id = this._hoveredId;
if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id }, { hover: false });
if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id }, { hover: false });
}
const [{ id }] = e.features;
if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id }, { hover: true });
if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id }, { hover: true });
this._hoveredId = id;
}
if (this._options.showPopup) {
const [{ properties: detail }] = e.features;
this._popup.setLngLat(e.lngLat).setHTML(this._getPopupHtml(detail)).addTo(this._map);
}
};
_onMouseLeave = () => {
if (this._map.getZoom() < this._options.minZoom) return;
this._map.getCanvas().style.cursor = '';
if (this._hoveredId !== null) {
if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id: this._hoveredId }, { hover: false });
if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id: this._hoveredId }, { hover: false });
}
this._hoveredId = null;
if (this._options.showPopup) {
this._popup.remove();
}
};
_onClick = e => {
if (this._map.getZoom() < this._options.minZoom) return;
if (!e.features.length) return;
const [{ properties: detail }] = e.features;
const { points } = this._tracksStore.find(({ [this._trackKey]: key }) => key === detail.id) || {};
this._trigger('click', { ...detail, points: (points || []).map(item => ({ ...item })) });
};
// 缩放到所有轨迹总边界
fitView({ top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
if (!this._tracksStore.length) return;
this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
this._map.fitBounds(this._boundingBox, {
duration: 2000,
padding: {
top: top + cut,
bottom: bottom + cut,
left: left + cut,
right: right + cut,
},
});
}
_clear() {
if (!this._map) return;
if (this._map.getSource(SOURCE_RUNNING_TRACK_LINE)) {
this._map.off('mousemove', LAYER_RUNNING_TRACK_LINE, this._onMouseMove);
this._map.off('mouseleave', LAYER_RUNNING_TRACK_LINE, this._onMouseLeave);
this._map.off('click', LAYER_RUNNING_TRACK_LINE, this._onClick);
this._map.removeLayer(LAYER_RUNNING_TRACK_LINE);
this._map.removeSource(SOURCE_RUNNING_TRACK_LINE);
}
if (this._map.getSource(SOURCE_WORKING_TRACK_LINE)) {
this._map.removeLayer(LAYER_WORKING_TRACK_LINE);
this._map.removeSource(SOURCE_WORKING_TRACK_LINE);
}
if (this._map.getSource(SOURCE_START_TRACK_POINT)) {
this._map.removeLayer(LAYER_START_TRACK_POINT);
this._map.removeSource(SOURCE_START_TRACK_POINT);
}
if (this._map.getSource(SOURCE_END_TRACK_POINT)) {
this._map.removeLayer(LAYER_END_TRACK_POINT);
this._map.removeSource(SOURCE_END_TRACK_POINT);
}
if (this._options.showPopup) {
this._popup.remove();
}
}
destroy() {
this._clear();
this._map = null;
this._tracksStore = [];
this._getPopupHtml = () => 'Nothing';
this.initOptions();
this.initStyle();
}
}
export default new TrackRenderer();

27
src/layout/MainContainer.vue

@ -3,10 +3,11 @@
import MapLayer from '@/layout/MapLayer.vue';
import SideMenu from '@/layout/components/SideMenu.vue';
import TopBar from '@/layout/components/TopBar.vue';
import LiveDialog from '@/layout/components/LiveDialog.vue';
import PasswordEditor from '@/layout/components/PasswordEditor.vue';
</script>
<template>
<MapLayer />
<t-layout :class="s.root">
<t-header>
<TopBar />
@ -16,10 +17,14 @@
<SideMenu />
</t-aside>
<t-content>
<MapLayer />
<RouterView />
</t-content>
</t-layout>
</t-layout>
<LiveDialog />
<PasswordEditor />
</template>
<style lang="less" module="s">
@ -27,22 +32,28 @@
z-index: 1;
position: relative;
height: 100%;
pointer-events: none;
//pointer-events: none;
color: var(--td-text-color-primary);
:global {
.t-layout__header {
pointer-events: auto;
box-shadow: 0 0 2px 0 var(--td-component-border);
z-index: 2;
//pointer-events: auto;
//background-image: url("../assets/top.png");
//background-repeat: no-repeat;
//background-position: left bottom;
background-color: black;
color: white;
//border-bottom: 1px solid var(--td-component-stroke);
z-index: 4;
}
.t-layout__sider {
width: fit-content;
background-color: transparent;
pointer-events: auto;
box-shadow: 0 0 1px 0 var(--td-component-border);
z-index: 1;
//pointer-events: auto;
//border-right: 1px solid var(--td-component-stroke);
//box-shadow: 0 0 1px 0 var(--td-component-border);
z-index: 3;
}
.t-layout--with-sider {

116
src/layout/MapLayer.vue

@ -1,12 +1,10 @@
<script setup>
import { onMounted } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import mapbox from 'mapbox-gl';
import config, { mapStyles } from '@/config/map';
import commonRefs from '@/utils/commonRefs';
import mapHelper from '@/core/mapHelper';
import 'mapbox-gl/dist/mapbox-gl.css';
import MultifunctionalBar from '@/layout/components/MultifunctionalBar.vue';
import ZoomBar from '@/layout/components/ZoomBar.vue';
let map;
@ -96,11 +94,12 @@
style: mapStyles.satelliteStreetMap,
center,
zoom,
minZoom: 3,
minZoom: 2.5,
maxZoom: 23,
dragRotate: false,
// antialias: true,
preserveDrawingBuffer: true,
projection: 'globe',
// preserveDrawingBuffer: true,
});
map.on('styledata', () => {
@ -110,6 +109,66 @@
});
map.on('style.load', () => {
map.setFog({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.02,
'space-color': 'rgb(11, 11, 25)',
'star-intensity': 0.35,
});
// map.addSource('mapbox-dem', {
// type: 'raster-dem',
// url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
// tileSize: 512,
// maxzoom: 14,
// });
// // add the DEM source as a terrain layer with exaggerated height
// map.setTerrain({ source: 'mapbox-dem', exaggeration: 1 });
//
// const { layers } = map.getStyle();
// const labelLayerId = layers.find(
// (layer) => layer.type === 'symbol' && layer.layout['text-field'],
// ).id;
//
// map.addLayer(
// {
// id: 'add-3d-buildings',
// source: 'composite',
// 'source-layer': 'building',
// filter: ['==', 'extrude', 'true'],
// type: 'fill-extrusion',
// minzoom: 10,
// paint: {
// 'fill-extrusion-color': '#aaa',
//
// // Use an 'interpolate' expression to
// // add a smooth transition effect to
// // the buildings as the user zooms in.
// 'fill-extrusion-height': [
// 'interpolate',
// ['linear'],
// ['zoom'],
// 15,
// 0,
// 15.05,
// ['get', 'height'],
// ],
// 'fill-extrusion-base': [
// 'interpolate',
// ['linear'],
// ['zoom'],
// 15,
// 0,
// 15.05,
// ['get', 'min_height'],
// ],
// 'fill-extrusion-opacity': 0.6,
// },
// },
// labelLayerId,
// );
commonRefs.setRef('map', map);
mapHelper.setMap(map);
});
@ -123,17 +182,24 @@
}
onMounted(() => {
initMap();
setTimeout(() => {
initMap();
});
});
onUnmounted(() => {
setTimeout(() => {
if (map) {
map.remove();
commonRefs.removeRef('map');
map = null;
}
}, 100);
});
</script>
<template>
<div :class="s.root" id="map">
<div class="tool-bar">
<ZoomBar class="tool" />
<MultifunctionalBar class="tool" />
</div>
</div>
<div :class="s.root" id="map" />
</template>
<style lang="less" module="s">
@ -141,21 +207,23 @@
position: absolute;
left: 0;
top: 0;
z-index: 0;
width: 100%;
bottom: 0;
right: 0;
//z-index: 0;
//width: 100%;
height: 100%;
background-color: #2f4f4f;
//background-color: #2f4f4f;
:global {
.tool-bar {
position: absolute;
top: 50%;
right: var(--td-comp-margin-m);
transform: translateY(-50%);
display: grid;
justify-content: center;
gap: var(--td-comp-margin-l);
}
//.tool-bar {
// position: absolute;
// top: 50%;
// right: var(--td-comp-margin-m);
// transform: translateY(-50%);
// display: grid;
// justify-content: center;
// gap: var(--td-comp-margin-l);
//}
}
}
</style>

108
src/layout/components/LiveDialog.vue

@ -0,0 +1,108 @@
<script setup>
// import BasePanel from '@/components/BasePanel.vue';
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
// import LivePreset from 'xgplayer/es/presets/live';
import HlsPlugin from 'xgplayer-hls';
import { onMounted, ref, onUnmounted } from 'vue';
import eventBus from '@/utils/eventBus';
import { storeToRefs } from 'pinia';
import { useLiveStore } from '@/stores';
const liveStore = useLiveStore();
const { liveUrl } = storeToRefs(liveStore);
console.log(liveUrl);
// console.log(liveUrl);
const visible = ref(false);
const playerRef = ref();
const player = ref();
onMounted(() => {
const options = {
// id: 'player',
el: playerRef.value,
url: '', // HLS
// url: 'http://pull.jiagutech.com/sgcloud/1736050936087842816.m3u8', // HLS
// url: 'http://pull.jiagutech.com/live/test.m3u8', // HLS
type: 'hls',
ignores: ['progress', 'time', 'playbackrate', 'pip'],
// fluid: true,
autoplayMuted: false,
autoplay: false,
plugins: [HlsPlugin],
lang: 'zh',
width: '100%',
height: '100%',
};
player.value = new Player(options);
eventBus.on('show-live-dialog', () => {
if (player.value) {
// player.value.resetState();
player.value.playNext({
url: liveUrl.value?.m3u8Url,
autoplayMuted: true,
autoplay: true,
});
}
visible.value = true;
});
});
onUnmounted(() => {
if (player.value) player.value.destroy();
eventBus.off('show-live-dialog');
});
function onCancel() {
//
}
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
mode="full-screen"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">直播</div>
</template>
<div class="container">
<div ref="playerRef" />
</div>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
:global {
.container {
height: 100%;
border-radius: var(--td-radius-medium);
overflow: hidden;
display: flex;
.video-player {
flex: 1;
margin-left: var(--td-comp-margin-s);
margin-top: var(--td-comp-margin-s);
border-radius: var(--td-radius-medium);
}
}
.t-dialog__position_fullscreen {
height: 100%;
}
}
}
</style>

90
src/layout/components/MultifunctionalBar.vue

@ -2,83 +2,76 @@
import { onMounted, ref, watchEffect } from 'vue';
import commonRefs from '@/utils/commonRefs';
import { useGlobalSettings } from '@/views/common/useGlobalSettings';
import mapConfig from '@/config/map';
import eventBus from '@/utils/eventBus';
// import { useGlobalFarm } from '@/views/common/useGlobalFarm';
// import { useGlobalFields } from '@/views/common/useGlobalFields';
import eventBus from '@/utils/eventBus';
// import eventBus from '@/utils/eventBus';
const isReady = ref(false);
const showMapHd = ref(true);
const showFieldFill = ref(true);
const showFieldName = ref(true);
const showNoFlyZone = ref(true);
// const showFieldFill = ref(true);
// const showFieldName = ref(true);
watchEffect(() => {
const settings = useGlobalSettings().valueOf();
showMapHd.value = settings.showMapHd;
showFieldFill.value = settings.showFieldFill;
showFieldName.value = settings.showFieldName;
showNoFlyZone.value = settings.showNoFlyZone;
// showFieldFill.value = settings.showFieldFill;
// showFieldName.value = settings.showFieldName;
});
function init() {
eventBus.emit('show-map-hd-layer', showMapHd.value);
}
// function init() {
// eventBus.emit('show-map-hd-layer', showMapHd.value);
// }
function onToggleMapHd() {
const result = !showMapHd.value;
eventBus.emit('show-map-hd-layer', result);
useGlobalSettings().set('showMapHd', result);
}
// function onToggleNoFlyZone() {
// // const result = !showMapHd.value;
// // eventBus.emit('show-map-hd-layer', result);
// // useGlobalSettings().set('showMapHd', result);
// }
function onToggleFill(val) {
function onToggleNoFlyZone(val) {
// useGlobalFields().toggleFill(val);
useGlobalSettings().set('showFieldFill', val);
useGlobalSettings().set('showNoFlyZone', val);
}
function onToggleName(val) {
// useGlobalFields().toggleName(val);
useGlobalSettings().set('showFieldName', val);
}
// function onToggleName(val) {
// // useGlobalFields().toggleName(val);
// useGlobalSettings().set('showFieldName', val);
// }
let map;
onMounted(async () => {
await commonRefs.getRef('map');
map = await commonRefs.getRef('map');
isReady.value = true;
init();
// init();
});
function onResetZoom() {
map.setZoom(mapConfig.zoom);
map.setCenter(mapConfig.center);
// map.setPitch(0);
// map.setBearing(0);
eventBus.emit('hide-all-panels');
}
</script>
<template>
<div :class="s.root" v-if="isReady">
<t-tooltip content="视角回归" placement="left">
<div class="cell">
<font-icon name="icon-positioning" />
<div class="cell" @click="onResetZoom">
<t-icon name="earth" />
</div>
</t-tooltip>
<t-popup placement="left-bottom" show-arrow :overlay-class-name="s.popup_free">
<div class="cell">
<font-icon name="icon-layer" />
</div>
<template #content>
<div class="top">
<t-space size="4px" direction="vertical" align="center" :class="[showMapHd ? 'active' : '']">
<div class="icon" @click="onToggleMapHd">
<img src="@/assets/map-icon-hd.png" alt="">
</div>
<span>高清图层</span>
</t-space>
</div>
</template>
</t-popup>
<t-popup placement="left-bottom" show-arrow :overlay-class-name="s.popup_single_col">
<div class="cell">
<font-icon name="icon-config" />
<t-icon name="setting" />
</div>
<template #content>
<t-space size="small" align="center">
<t-switch :label="['显示', '隐藏']" :value="showFieldFill" @change="onToggleFill" />
<span>地块形状</span>
</t-space>
<t-space size="small" align="center">
<t-switch :label="['显示', '隐藏']" :value="showFieldName" @change="onToggleName" />
<span>地块名称</span>
<t-switch :label="['显示', '隐藏']" :value="showNoFlyZone" @change="onToggleNoFlyZone" />
<span>禁飞区</span>
</t-space>
</template>
</t-popup>
@ -138,6 +131,9 @@
}
.root {
position: absolute;
bottom: 40px;
right: 10px;
background-color: fade(black, 35%);
backdrop-filter: blur(6px);
border-radius: 100px;

105
src/layout/components/PasswordEditor.vue

@ -0,0 +1,105 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { useAuthStore } from '@/stores';
import eventBus from '@/utils/eventBus';
import { storeToRefs } from 'pinia';
// import ImageUploader from '@/components/ImageUploader.vue';
// import { update } from '@/utils/helpers';
const visible = ref(false);
const authStore = useAuthStore();
const { userInfo } = storeToRefs(authStore);
const { updatePassword } = authStore;
const form = ref();
const formData = ref({
id: undefined,
phone: undefined,
oldPassword: undefined,
newPassword: undefined,
});
const FORM_RULES = {
phone: [{ required: true, message: '请输入' }],
oldPassword: [{ required: true, message: '请输入原密码' }],
newPassword: [{ required: true, message: '请输入新密码' }],
};
function onCancel() {
visible.value = false;
form.value.reset();
}
function onSubmit({ validateResult }) {
if (validateResult === true) {
updatePassword(formData.value).then(() => {
MessagePlugin.success('更改成功');
onCancel();
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
}
onMounted(() => {
eventBus.on('show-change-password', () => {
formData.value.id = userInfo.value.id;
formData.value.phone = userInfo.value.phone;
visible.value = true;
});
});
onUnmounted(() => {
eventBus.off('show-change-password');
});
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
:close-btn="false"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">更改密码</div>
</template>
<t-form
ref="form"
:data="formData"
:rules="FORM_RULES"
colon
@submit="onSubmit"
>
<t-form-item name="id" v-show="false" />
<t-form-item label="手机号" name="phone">
<t-input v-model="formData.phone" disabled />
</t-form-item>
<t-form-item label="原密码" name="oldPassword">
<t-input v-model="formData.oldPassword" />
</t-form-item>
<t-form-item label="新密码" name="newPassword">
<t-input v-model="formData.newPassword" />
</t-form-item>
<t-form-item>
<t-space>
<t-button theme="primary" type="submit">提交</t-button>
<t-button theme="default" @click="onCancel">取消</t-button>
</t-space>
</t-form-item>
</t-form>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
}
</style>

161
src/layout/components/SideMenu.vue

@ -1,17 +1,30 @@
<script setup>
import { computed, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import commonRefs from '@/utils/commonRefs';
import { storeToRefs } from 'pinia';
import { useAuthStore } from '@/stores';
const {
isPlatform,
isManufacturer,
isRegulator,
isPilot,
} = storeToRefs(useAuthStore());
const collapsed = ref(false);
const changeCollapsed = () => {
collapsed.value = !collapsed.value;
commonRefs.getRef('map').then((map) => {
map.resize();
});
};
const route = useRoute();
const router = useRouter();
const currentViewName = computed({
get() {
return route?.meta?.group || route.name || 'ExampleView';
return route?.meta?.group || route.name || 'MonitorView';
},
set(nv) {
router.push({
@ -22,65 +35,155 @@
</script>
<template>
<t-menu v-model="currentViewName" :class="s.root" :collapsed="collapsed">
<t-menu v-model="currentViewName" theme="dark" :class="s.root" :collapsed="collapsed">
<template #logo>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<template #icon><t-icon :name="collapsed ? 'indent-right' : 'indent-left'" size="large" /></template>
</t-button>
<div class="t-demo-collapse-btn" @click="changeCollapsed">
<t-icon :name="collapsed ? 'indent-right' : 'indent-left'" size="large" />
</div>
<!-- <t-button class="t-demo-collapse-btn" variant="text" block shape="square" @click="changeCollapsed">-->
<!-- <template #icon></template>-->
<!-- </t-button>-->
</template>
<t-menu-item value="ExampleView">
<t-menu-item value="ExampleView" v-if="false">
<template #icon>
<font-icon name="icon-plant-height" />
<t-icon name="server" />
</template>
案例模板
</t-menu-item>
<t-menu-item value="item1">
<t-menu-item value="MonitorView" v-if="isPlatform || isManufacturer">
<template #icon>
<font-icon name="icon-plant-height" />
<img class="img-icon" src="../../assets/list_icon_shishijiankong.png">
<!-- <font-icon name="icon-no-fly-zone" />-->
</template>
实时监控
<div class="label">实时监控</div>
</t-menu-item>
<t-menu-item value="resource">
<t-menu-item value="DeviceView" v-if="isPlatform || isManufacturer">
<template #icon>
<t-icon name="server" />
<img class="img-icon" src="../../assets/list_icon_shebeiguanli.png">
<!-- <font-icon name="icon-no-fly-zone" />-->
</template>
设备管理
<div class="label">设备管理</div>
</t-menu-item>
<t-menu-item value="ManufacturerView">
<t-menu-item value="SortieView" v-if="isPlatform || isManufacturer">
<template #icon>
<t-icon name="root-list" />
<img class="img-icon" src="../../assets/list_icon_jiaciguanli.png">
<!-- <font-icon name="icon-no-fly-zone" />-->
</template>
制造商管理
<div class="label">架次管理</div>
</t-menu-item>
<t-menu-item value="RegulatorView">
<t-menu-item value="NoFlyZoneView" v-if="isPlatform || isManufacturer">
<template #icon>
<t-icon name="root-list" />
<img class="img-icon" src="../../assets/list_icon_jinfeiquguanli.png">
<!-- <font-icon name="icon-no-fly-zone" />-->
</template>
监管者管理
<div class="label">禁飞区管理</div>
</t-menu-item>
<t-menu-item value="1">
<t-menu-item value="AchievementView" v-if="isRegulator || isPilot">
<template #icon>
<t-icon name="server" />
<img class="img-icon" src="../../assets/list_icon_jinfeiquguanli.png">
<!-- <font-icon name="icon-achievement" />-->
</template>
架次管理
<div class="label">成果管理</div>
</t-menu-item>
<t-menu-item value="NoFlyZoneView">
<t-menu-item value="ManufacturerView" v-if="isPlatform">
<template #icon>
<t-icon name="server" />
<img class="img-icon" src="../../assets/list_icon_zhizaoshangguanli.png">
<!-- <font-icon name="icon-achievement" />-->
</template>
禁飞区管理
<div class="label">制造商管理</div>
</t-menu-item>
<t-menu-item value="3">
<t-menu-item value="RegulatorView" v-if="isPlatform">
<template #icon>
<t-icon name="server" />
<img class="img-icon" src="../../assets/list_icon_shishijiankong.png">
<!-- <font-icon name="icon-achievement" />-->
</template>
数据分析
<div class="label">监管者管理</div>
</t-menu-item>
<t-menu-item value="3" v-if="false">
<template #icon>
<img class="img-icon" src="../../assets/list_icon_shishijiankong.png">
<!-- <font-icon name="icon-achievement" />-->
</template>
<div class="label">数据分析</div>
</t-menu-item>
</t-menu>
</template>
<style lang="less" module="s">
.root {
//
transition: unset;
//padding: 0;
:global {
.img-icon {
height: 20px;
width: 20px;
object-fit: contain;
}
.label {
margin-left: var(--td-comp-margin-s);
}
.t-menu {
padding: 0;
}
.t-default-menu__inner .t-menu__logo:not(:empty) {
height: auto;
}
.t-default-menu.t-is-collapsed .t-menu__logo > * {
margin-left: 0 !important;
}
.t-menu__logo > * {
margin-left: 0 !important;
}
.t-menu__item.t-is-active {
background-color: transparent !important;
border-radius: unset;
background-image: linear-gradient(to right, transparent, var(--td-brand-color));
position: relative;
&::before {
position: absolute;
content: url("../../assets/list_chosen.png");
top: 50%;
left: 0;
transform: translateY(-43%);
}
}
.t-menu__item:hover:not(.t-is-active):not(.t-is-opened):not( .t-is-disabled) {
border-radius: unset;
background-color: transparent;
background-image: linear-gradient(to right, transparent, var(--td-brand-color));
}
.t-demo-collapse-btn {
width: 100%;
cursor: pointer;
padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-l);
text-align: center;
color: white;
border-radius: unset;
transition: unset;
&:hover, &:focus-visible {
//border-color: black;
border: unset;
background-color: transparent;
background-image: linear-gradient(to right, transparent, var(--td-brand-color));
}
//.t-button--variant-text:hover, .t-button--variant-text:focus-visible {
// background-color: var(--td-brand-color) !important;
//}
//.t-button:not(.t-is-disabled):not(.t-button--ghost) {
// --ripple-color: var(--td-brand-color) !important;
//}
}
}
}
</style>

148
src/layout/components/TopBar.vue

@ -1,9 +1,33 @@
<script setup>
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { DialogPlugin } from 'tdesign-vue-next';
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
import { storeToRefs } from 'pinia';
import { useLiveStore, useAuthStore } from '@/stores';
import eventBus from '@/utils/eventBus';
import RegulatorLogo from '@/assets/regulator_log.png';
import ManufacturerLogo from '@/assets/m_logo.png';
const { getLiveUrl } = useLiveStore();
const { userInfo, isRegulator, isManufacturer } = storeToRefs(useAuthStore());
const router = useRouter();
const logoImg = computed(() => {
if (userInfo.value.logo) {
return userInfo.value.logo;
}
if (isManufacturer.value) {
return ManufacturerLogo;
}
if (isRegulator.value) {
return RegulatorLogo;
}
return 'https://tdesign.gtimg.com/demo/demo-image-1.png';
});
function LogOut() {
const dialog = DialogPlugin({
header: '操作确认',
@ -17,49 +41,115 @@
},
});
}
const code = ref();
function onWatchLive() {
if (!code.value) {
MessagePlugin.warning('请输入直播分享码');
return;
}
getLiveUrl({ id: code.value }).then(() => {
eventBus.emit('show-live-dialog');
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
// console.log('aaaa');
}
function onShow() {
eventBus.emit('show-change-password');
}
</script>
<template>
<div :class="s.root">
<t-space align="center">
<t-image
src="https://tdesign.gtimg.com/demo/demo-image-1.png"
:style="{ width: '38px', height: '38px' }"
shape="circle"
fit="cover"
/>
<div class="title">云端无人机管理系统管理员</div>
</t-space>
<t-space align="center">
<t-button theme="primary" shape="round" variant="base">监察者模式</t-button>
<t-dropdown>
<t-avatar>
<template #icon>
<t-icon name="user" />
<div class="bg-img">
<img src="../../assets/top.png">
</div>
<div class="content">
<t-space align="center">
<t-image
:src="logoImg"
:style="{ width: '38px', height: '38px' }"
fit="cover"
shape="circle"
/>
<div class="title">{{ userInfo.companyName || userInfo.username }}</div>
</t-space>
<t-space align="center">
<t-input-adornment v-if="isRegulator">
<t-input v-model="code" placeholder="请输入直播分享码" clearable />
<template #append>
<t-button @click="onWatchLive">观看</t-button>
</template>
</t-avatar>
<t-dropdown-menu>
<t-dropdown-item @click="LogOut">
退出登录
</t-dropdown-item>
</t-dropdown-menu>
</t-dropdown>
</t-space>
</t-input-adornment>
<t-dropdown>
<t-avatar>
<template #icon>
<t-icon name="user" />
</template>
</t-avatar>
<t-dropdown-menu>
<t-dropdown-item @click="onShow">
更改密码
</t-dropdown-item>
<t-dropdown-item @click="LogOut">
退出登录
</t-dropdown-item>
</t-dropdown-menu>
</t-dropdown>
</t-space>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-xl);
position: relative;
height: 100%;
box-sizing: border-box;
//&:global::before {
// content: url("../../assets/top.png");
// position: absolute;
// top: 0;
// bottom: 0;
// left: 0;
// right: 0;
//}
:global {
.content {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-xl);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.title {
white-space: nowrap;
font-size: var(--td-font-size-headline-medium);
font-weight: bold;
}
.bg-img {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
img {
height: 100%;
}
}
}
}

2
src/main.js

@ -4,7 +4,9 @@ import router from '@/router';
import TDesignVue from '@/plugins/TDesign-vue'; // TDesign按需引入
import '@/plugins/components';
import '@/plugins/directives';
import '@/styles/theme.css';
import '@/styles/common.less'; // 自定义全局样式表
import 'overlayscrollbars/overlayscrollbars.css';
app.use(store);
app.use(router);

2
src/plugins/TDesign-vue.js

@ -120,12 +120,14 @@ import {
Popup,
Upload,
ConfigProvider,
Statistic,
} from 'tdesign-vue-next';
export default {
install: (app) => {
// 在这里编写插件代码
app.use(Button);
app.use(Statistic);
app.use(Tabs);
app.use(TabPanel);
app.use(Icon);

34
src/plugins/components.js

@ -47,3 +47,37 @@ app.component('DataDispatcher', {
return self?.$slots?.default(self.payload);
},
});
// /**
// * 单位组件
// * 自动给值加单位,用指定tag包裹
// * 示例1:<unit use="千克" tag="span">{{ 100 }}</unit>(结果:100<span>千克</span>)
// * 示例2:<unit use="千克" tag="span">{{ null }}</unit>(结果:-)
// */
// // eslint-disable-next-line vue/multi-word-component-names
// app.component('unit', {
// // functional: true,
//
// props: {
// use: {
// type: String,
// required: true,
// },
// tag: {
// type: String,
// default: 'em',
// },
// falsyTo: {
// type: String,
// default: '-',
// },
// },
//
// render(createElement, context) {
// console.log('aa', createElement, context);
// const [{ text }] = context.children;
// const { tag, use, falsyTo } = context.props;
// const unitNode = createElement(tag, use);
// return text ? context.children.concat(unitNode) : [falsyTo];
// },
// });

4
src/plugins/directives.js

@ -6,7 +6,6 @@ app.directive('scrollbar', (el, binding) => {
const { arg: enable } = binding;
if (!enable) return;
const { padding } = binding.modifiers;
console.log(padding);
OverlayScrollbars(el, {
paddingAbsolute: !!padding,
overflow: {
@ -14,7 +13,8 @@ app.directive('scrollbar', (el, binding) => {
y: 'scroll',
},
scrollbars: {
theme: 'os-theme-light',
// theme: 'os-theme-light',
theme: 'os-theme-dark',
autoHide: 'leave',
autoHideDelay: 300,
},

24
src/router/index.js

@ -1,5 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router';
import auth from '@/utils/auth';
import { storeToRefs } from 'pinia';
import { useAuthStore } from '@/stores';
const ExampleView = () => import('@/views/ExampleView/ExampleView.vue');
const LoginView = () => import('@/views/LoginView/LoginView.vue');
@ -7,11 +9,23 @@ const NoFlyZoneView = () => import('@/views/NoFlyZoneView/NoFlyZoneView.vue');
const NoFlyZoneEditorView = () => import('@/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue');
const ManufacturerView = () => import('@/views/ManufacturerView/ManufacturerView.vue');
const RegulatorView = () => import('@/views/RegulatorView/RegulatorView.vue');
const DeviceView = () => import('@/views/DeviceView/DeviceView.vue');
const MonitorView = () => import('@/views/MonitorView/MonitorView.vue');
const SortieView = () => import('@/views/SortieView/SortieView.vue');
const AchievementView = () => import('@/views/AchievementView/AchievementView.vue');
const routes = [
{
path: '/',
redirect: () => '/example',
redirect: () => {
const {
// isPlatform,
// isManufacturer,
isRegulator,
isPilot,
} = storeToRefs(useAuthStore());
return ((isPilot.value || isRegulator.value) ? '/achievements' : '/monitor');
},
},
{ path: '/example', name: 'ExampleView', component: ExampleView },
@ -27,6 +41,14 @@ const routes = [
{ path: '/manufacturers', name: 'ManufacturerView', component: ManufacturerView },
{ path: '/regulators', name: 'RegulatorView', component: RegulatorView },
{ path: '/devices', name: 'DeviceView', component: DeviceView },
{ path: '/monitor', name: 'MonitorView', component: MonitorView },
{ path: '/sorties', name: 'SortieView', component: SortieView },
{ path: '/achievements', name: 'AchievementView', component: AchievementView },
];
const router = createRouter({

6
src/stores/index.js

@ -11,3 +11,9 @@ export * from './modules/noFlyZoneStore';
export * from './modules/authStore';
export * from './modules/manufacturerStore';
export * from './modules/regulatorStore';
export * from './modules/deviceStore';
export * from './modules/sortieStore';
export * from './modules/mediaStore';
export * from './modules/achievementStore';
export * from './modules/liveStore';
export * from './modules/monitorStore';

111
src/stores/modules/achievementStore.js

@ -0,0 +1,111 @@
/**
* 成果管理
*/
import { ref } from 'vue';
import { defineStore } from 'pinia';
import http from '@/utils/http';
import * as urls from '@/config/urls';
import * as helpers from '@/utils/helpers';
export const useAchievementStore = defineStore('achievement', () => {
const achievementList = ref([]);
const achievementExtra = ref({ total: null });
const achievementQueries = ref({ page: 1, pageSize: 10 });
// const achievementDetail = ref({});
function getAchievementList(otherQueries = {}) {
return http.getInstance().get(urls.GET_ACHIEVEMENT_LIST, {
params: { ...achievementQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: list, extra } = data;
achievementList.value = list || [];
achievementExtra.value = { ...achievementExtra.value, ...(extra || {}) };
achievementQueries.value = { ...achievementQueries.value, ...otherQueries };
return data;
});
}
// function getAchievementDetail(achievementId = '') {
// const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, achievementId);
// return http.getInstance().get(url).then(({ data }) => {
// const { data: detail } = data || {};
// achievementDetail.value = detail;
// return data;
// });
// }
function createAchievementShareCode(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'ids',
]);
return http.getInstance().post(urls.CREATE_ACHIEVEMENT_SHARE_CODE, reqData).then(({ data }) => {
if (refreshList) getAchievementList();
return data;
});
}
function bindingAchievement(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'id',
]);
const url = helpers.buildURL(urls.BINDING_ACHIEVEMENT, reqData.id);
return http.getInstance().post(url).then(({ data }) => {
if (refreshList) getAchievementList();
return data;
});
}
//
// function updateAchievement(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// 'manufacturerId',
// 'sn',
// 'type',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
// return http.getInstance().put(url, reqData).then(({ data }) => {
// if (refreshList) getAchievementList();
// return data;
// });
// }
//
// function deleteAchievement(achievementId = '', { refreshList = false } = {}) {
// const url = helpers.buildURL(urls.DELETE_SORTIE, achievementId);
// return http.getInstance().delete(url).then(({ data }) => {
// if (refreshList) getAchievementList();
// return data;
// });
// }
//
// function updateAchievementState(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
// return http.getInstance().put(url).then(({ data }) => {
// if (refreshList) getAchievementList();
// return data;
// });
// }
return {
achievementList,
achievementQueries,
achievementExtra,
// achievementDetail,
getAchievementList,
// createAchievement,
// updateAchievement,
// deleteAchievement,
// updateAchievementState,
// getAchievementDetail,
createAchievementShareCode,
bindingAchievement,
};
});
export default null;

23
src/stores/modules/authStore.js

@ -1,7 +1,7 @@
/**
* authStore
*/
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import http from '@/utils/http';
import * as helpers from '@/utils/helpers';
@ -13,6 +13,11 @@ export const useAuthStore = defineStore('auth', () => {
// state
const userInfo = ref(UserInfo.get());
const isPlatform = computed(() => userInfo.value.roleId === 1);
const isManufacturer = computed(() => userInfo.value.roleId === 2);
const isRegulator = computed(() => userInfo.value.roleId === 3);
const isPilot = computed(() => userInfo.value.roleId === 4);
// const isPlatform = computed(() => {
// const { roles } = userInfo;
// return (roles || []).includes('platform');
@ -47,11 +52,27 @@ export const useAuthStore = defineStore('auth', () => {
});
}
function updatePassword(formData = {}) {
const reqData = helpers.pick(formData, [
'id',
'oldPassword',
'newPassword',
]);
const url = helpers.buildURL(urls.UPDATE_PASSWORD, reqData.id);
return http.getInstance().put(url, reqData).then(({ data }) => data);
}
return {
userInfo,
// isPlatform,
// isFarmer,
loginWithPassword,
updatePassword,
isRegulator,
isPlatform,
isManufacturer,
isPilot,
};
});

100
src/stores/modules/deviceStore.js

@ -0,0 +1,100 @@
/**
* 设备管理
*/
import { ref } from 'vue';
import { defineStore } from 'pinia';
import http from '@/utils/http';
import * as urls from '@/config/urls';
import * as helpers from '@/utils/helpers';
export const useDeviceStore = defineStore('device', () => {
const deviceList = ref([]);
const deviceExtra = ref({ total: null });
const deviceQueries = ref({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined });
const deviceDetail = ref({});
function getDeviceList(otherQueries = {}) {
return http.getInstance().get(urls.GET_DEVICE_LIST, {
params: { ...deviceQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: list, extra } = data;
deviceList.value = list || [];
deviceExtra.value = { ...deviceExtra.value, ...(extra || {}) };
deviceQueries.value = { ...deviceQueries.value, ...otherQueries };
return data;
});
}
function getDeviceDetail(deviceId = '') {
const url = helpers.buildURL(urls.GET_DEVICE_DETAIL, deviceId);
return http.getInstance().get(url).then(({ data }) => {
const { data: detail } = data || {};
deviceDetail.value = detail;
return data;
});
}
function createDevice(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'manufacturerId',
'sn',
'type',
]);
return http.getInstance().post(urls.CREATE_DEVICE, reqData).then(({ data }) => {
if (refreshList) getDeviceList();
return data;
});
}
function updateDevice(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'id',
'manufacturerId',
'sn',
'type',
]);
const url = helpers.buildURL(urls.UPDATE_DEVICE, reqData.id);
return http.getInstance().put(url, reqData).then(({ data }) => {
if (refreshList) getDeviceList();
return data;
});
}
function deleteDevice(deviceId = '', { refreshList = false } = {}) {
const url = helpers.buildURL(urls.DELETE_DEVICE, deviceId);
return http.getInstance().delete(url).then(({ data }) => {
if (refreshList) getDeviceList();
return data;
});
}
function updateDeviceState(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'id',
]);
const url = helpers.buildURL(urls.UPDATE_DEVICE_LOCKED, reqData.id);
return http.getInstance().put(url).then(({ data }) => {
if (refreshList) getDeviceList();
return data;
});
}
return {
deviceList,
deviceQueries,
deviceExtra,
deviceDetail,
getDeviceList,
createDevice,
updateDevice,
deleteDevice,
updateDeviceState,
getDeviceDetail,
};
});
export default null;

110
src/stores/modules/liveStore.js

@ -0,0 +1,110 @@
/**
* 直播
*/
import { ref } from 'vue';
import { defineStore } from 'pinia';
import http from '@/utils/http';
import * as urls from '@/config/urls';
import * as helpers from '@/utils/helpers';
export const useLiveStore = defineStore('live', () => {
const liveUrl = ref({});
const liveQueries = ref({ id: undefined });
function getLiveUrl(otherQueries = {}) {
const url = helpers.buildURL(urls.GET_PULL_STREAM_URL, otherQueries.id);
return http.getInstance().get(url, {
params: { ...liveQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: Urls } = data;
liveUrl.value = Urls || {};
// liveExtra.value = { ...liveExtra.value, ...(extra || {}) };
liveQueries.value = { ...liveQueries.value, ...otherQueries };
return data;
});
}
// function getLiveDetail(liveId = '') {
// const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, liveId);
// return http.getInstance().get(url).then(({ data }) => {
// const { data: detail } = data || {};
// liveDetail.value = detail;
// return data;
// });
// }
// function createLiveShareCode(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'ids',
// ]);
//
// return http.getInstance().post(urls.CREATE_ACHIEVEMENT_SHARE_CODE, reqData).then(({ data }) => {
// if (refreshList) getLiveList();
// return data;
// });
// }
// function bindingLive(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// ]);
// const url = helpers.buildURL(urls.BINDING_ACHIEVEMENT, reqData.id);
// return http.getInstance().post(url).then(({ data }) => {
// if (refreshList) getLiveList();
// return data;
// });
// }
//
// function updateLive(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// 'manufacturerId',
// 'sn',
// 'type',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
// return http.getInstance().put(url, reqData).then(({ data }) => {
// if (refreshList) getLiveList();
// return data;
// });
// }
//
// function deleteLive(liveId = '', { refreshList = false } = {}) {
// const url = helpers.buildURL(urls.DELETE_SORTIE, liveId);
// return http.getInstance().delete(url).then(({ data }) => {
// if (refreshList) getLiveList();
// return data;
// });
// }
//
// function updateLiveState(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
// return http.getInstance().put(url).then(({ data }) => {
// if (refreshList) getLiveList();
// return data;
// });
// }
return {
liveUrl,
liveQueries,
getLiveUrl,
// liveExtra,
// liveDetail,
// getLiveList,
// createLive,
// updateLive,
// deleteLive,
// updateLiveState,
// getLiveDetail,
// createLiveShareCode,
// bindingLive,
};
});
export default null;

8
src/stores/modules/manufacturerStore.js

@ -12,14 +12,14 @@ export const useManufacturerStore = defineStore('manufacturer', () => {
const manufacturerExtra = ref({ total: null });
const manufacturerQueries = ref({ page: 1, pageSize: 10, all: undefined, search: undefined });
function getManufacturerList(otherQueries = {}) {
function getManufacturerList(otherQueries = {}, { mergeArgs = true, mergeData = true } = {}) {
return http.getInstance().get(urls.GET_MANUFACTURER_LIST, {
params: { ...manufacturerQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: list, extra } = data;
manufacturerList.value = list || [];
manufacturerExtra.value = { ...manufacturerExtra.value, ...(extra || {}) };
manufacturerQueries.value = { ...manufacturerQueries.value, ...otherQueries };
if (mergeData) manufacturerList.value = list || [];
if (mergeData) manufacturerExtra.value = { ...manufacturerExtra.value, ...(extra || {}) };
if (mergeArgs) manufacturerQueries.value = { ...manufacturerQueries.value, ...otherQueries };
return data;
});
}

55
src/stores/modules/mediaStore.js

@ -0,0 +1,55 @@
/**
* 媒体管理
*/
import { ref } from 'vue';
import { defineStore } from 'pinia';
import http from '@/utils/http';
import * as urls from '@/config/urls';
import * as helpers from '@/utils/helpers';
export const useMediaStore = defineStore('media', () => {
const mediaList = ref([]);
const mediaQueries = ref({ sortieId: undefined });
function getMediaList(sortieId = '') {
const id = sortieId || mediaQueries.value.sortieId;
const url = helpers.buildURL(urls.GET_MEDIA_LIST, id);
return http.getInstance().get(url).then(({ data }) => {
const { data: list } = data;
mediaList.value = list || [];
mediaQueries.value = { mediaId: sortieId || mediaQueries.value.sortieId };
return data;
});
}
function createMedia(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'id',
'medias',
]);
const url = helpers.buildURL(urls.CREATE_MEDIA, reqData.id);
return http.getInstance().post(url, reqData).then(({ data }) => {
if (refreshList) getMediaList();
return data;
});
}
function deleteMedia(mediaId = '', { refreshList = false } = {}) {
const url = helpers.buildURL(urls.DELETE_MEDIA, mediaId);
return http.getInstance().delete(url).then(({ data }) => {
if (refreshList) getMediaList();
return data;
});
}
return {
mediaList,
mediaQueries,
getMediaList,
createMedia,
deleteMedia,
};
});
export default null;

124
src/stores/modules/monitorStore.js

@ -0,0 +1,124 @@
/**
* 实时监控
*/
import { ref } from 'vue';
import { defineStore } from 'pinia';
import mapConfig from '@/config/map';
import http from '@/utils/http';
import * as urls from '@/config/urls';
// import * as helpers from '@/utils/helpers';
export const useMonitorStore = defineStore('monitor', () => {
const mapOptions = ref({
currentZoom: mapConfig.zoom,
currentCenter: mapConfig.center,
currentRange: {
maxLat: 67.332486,
maxLng: 135.227994,
minLat: 18.186979,
minLng: 49.880052,
},
});
const monitorList = ref([]);
const monitorExtra = ref({ total: null });
const monitorQueries = ref({ });
// const monitorDetail = ref({});
function getMonitorList(otherQueries = {}) {
return http.getInstance().get(urls.GET_ONLINE_DEVICE, {
params: { ...monitorQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: list, extra } = data;
monitorList.value = list || [];
monitorExtra.value = { ...monitorExtra.value, ...(extra || {}) };
// monitorQueries.value = { ...monitorQueries.value, ...otherQueries };
return data;
});
}
// function getMonitorDetail(monitorId = '') {
// const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, monitorId);
// return http.getInstance().get(url).then(({ data }) => {
// const { data: detail } = data || {};
// monitorDetail.value = detail;
// return data;
// });
// }
// function createMonitorShareCode(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'ids',
// ]);
//
// return http.getInstance().post(urls.CREATE_ACHIEVEMENT_SHARE_CODE, reqData).then(({ data }) => {
// if (refreshList) getMonitorList();
// return data;
// });
// }
//
// function bindingMonitor(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// ]);
// const url = helpers.buildURL(urls.BINDING_ACHIEVEMENT, reqData.id);
// return http.getInstance().post(url).then(({ data }) => {
// if (refreshList) getMonitorList();
// return data;
// });
// }
//
// function updateMonitor(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// 'manufacturerId',
// 'sn',
// 'type',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
// return http.getInstance().put(url, reqData).then(({ data }) => {
// if (refreshList) getMonitorList();
// return data;
// });
// }
//
// function deleteMonitor(monitorId = '', { refreshList = false } = {}) {
// const url = helpers.buildURL(urls.DELETE_SORTIE, monitorId);
// return http.getInstance().delete(url).then(({ data }) => {
// if (refreshList) getMonitorList();
// return data;
// });
// }
//
// function updateMonitorState(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
// return http.getInstance().put(url).then(({ data }) => {
// if (refreshList) getMonitorList();
// return data;
// });
// }
return {
// monitorList,
// monitorQueries,
// monitorExtra,
// monitorDetail,
// getMonitorList,
// createMonitor,
// updateMonitor,
// deleteMonitor,
// updateMonitorState,
// getMonitorDetail,
// createMonitorShareCode,
// bindingMonitor,
getMonitorList,
mapOptions,
};
});
export default null;

8
src/stores/modules/noFlyZoneStore.js

@ -14,14 +14,14 @@ export const useNoFlyZoneStore = defineStore('noFlyZone', () => {
const noFlyZoneDetail = ref({});
function getNoFlyZoneList(otherQueries = {}) {
function getNoFlyZoneList(otherQueries = {}, { mergeArgs = true, mergeData = true } = {}) {
return http.getInstance().get(urls.GET_NO_FLY_ZONE_LIST, {
params: { ...noFlyZoneQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: list, extra } = data;
noFlyZoneList.value = list || [];
noFlyZoneExtra.value = { ...noFlyZoneExtra.value, ...(extra || {}) };
noFlyZoneQueries.value = { ...noFlyZoneQueries.value, ...otherQueries };
if (mergeData) noFlyZoneList.value = list || [];
if (mergeData) noFlyZoneExtra.value = { ...noFlyZoneExtra.value, ...(extra || {}) };
if (mergeArgs) noFlyZoneQueries.value = { ...noFlyZoneQueries.value, ...otherQueries };
return data;
});
}

1
src/stores/modules/regulatorStore.js

@ -26,6 +26,7 @@ export const useRegulatorStore = defineStore('regulator', () => {
function createRegulator(formData = {}, { refreshList = false } = {}) {
const reqData = helpers.pick(formData, [
'username',
'phone',
'password',
]);

87
src/stores/modules/sortieStore.js

@ -0,0 +1,87 @@
/**
* 架次管理
*/
import { ref } from 'vue';
import { defineStore } from 'pinia';
import http from '@/utils/http';
import * as urls from '@/config/urls';
import * as helpers from '@/utils/helpers';
export const useSortieStore = defineStore('sortie', () => {
const sortieList = ref([]);
const sortieExtra = ref({ total: null });
const sortieQueries = ref({ page: 1, pageSize: 10, search: undefined });
const sortieDetail = ref({});
function getSortieList(otherQueries = {}) {
return http.getInstance().get(urls.GET_SORTIE_LIST, {
params: { ...sortieQueries.value, ...otherQueries },
}).then(({ data }) => {
const { data: list, extra } = data;
sortieList.value = list || [];
sortieExtra.value = { ...sortieExtra.value, ...(extra || {}) };
sortieQueries.value = { ...sortieQueries.value, ...otherQueries };
return data;
});
}
function getSortieDetail(sortieId = '') {
const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, sortieId);
return http.getInstance().get(url).then(({ data }) => {
const { data: detail } = data || {};
sortieDetail.value = detail;
return data;
});
}
//
// function updateSortie(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// 'manufacturerId',
// 'sn',
// 'type',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
// return http.getInstance().put(url, reqData).then(({ data }) => {
// if (refreshList) getSortieList();
// return data;
// });
// }
//
// function deleteSortie(sortieId = '', { refreshList = false } = {}) {
// const url = helpers.buildURL(urls.DELETE_SORTIE, sortieId);
// return http.getInstance().delete(url).then(({ data }) => {
// if (refreshList) getSortieList();
// return data;
// });
// }
//
// function updateSortieState(formData = {}, { refreshList = false } = {}) {
// const reqData = helpers.pick(formData, [
// 'id',
// ]);
//
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
// return http.getInstance().put(url).then(({ data }) => {
// if (refreshList) getSortieList();
// return data;
// });
// }
return {
sortieList,
sortieQueries,
sortieExtra,
sortieDetail,
getSortieList,
// createSortie,
// updateSortie,
// deleteSortie,
// updateSortieState,
getSortieDetail,
};
});
export default null;

36
src/styles/common.less

@ -22,6 +22,7 @@ html,
.mapboxgl-popup.shape-editor-popup,
.mapboxgl-popup.line-editor-popup {
color: white;
.mapboxgl-popup-content {
background-color: fade(black, 60%);
}
@ -78,6 +79,7 @@ html,
}
.mapboxgl-popup.perception-popup {
color: white;
.mapboxgl-popup-content {
padding: 0;
background-color: fade(black, 60%);
@ -125,3 +127,37 @@ html,
.danger-bg-color {
background-color: #f5222d !important;
}
.t-image-viewer__modal-mask {
background-color: black !important;
//backdrop-filter: blur(30px);
}
.table-custom {
.t-table__header {
tr {
background-color: var(--td-brand-color);
//color: white;
}
th {
color: white;
}
}
.t-table__filter-icon:hover {
color: var(--td-brand-color-hover);
}
}
.vertical-line {
height: 100%;
width: 5px;
background-color: var(--td-brand-color);
}
.header-title {
font-weight: bold;
}

466
src/styles/theme.css

@ -0,0 +1,466 @@
:root,:root[theme-mode="light"] {
--brand-main: var(--td-brand-color-5);
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-4);
--td-brand-color: var(--td-brand-color-5);
--td-brand-color-active: var(--td-brand-color-6);
--td-brand-color-1: #eef4ff;
--td-brand-color-2: #d1e4ff;
--td-brand-color-3: #a3ccff;
--td-brand-color-4: #6bb2ff;
--td-brand-color-5: #0894fa;
--td-brand-color-6: #007ad3;
--td-brand-color-7: #0060a8;
--td-brand-color-8: #004881;
--td-brand-color-9: #00325c;
--td-brand-color-10: #00203f;
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e7e7e7;
--td-gray-color-4: #dcdcdc;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
/* 字体配置 */
--td-font-family: PingFang SC, Microsoft YaHei, Arial Regular;
--td-font-family-medium: PingFang SC, Microsoft YaHei, Arial Medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-common: 8px;
--td-line-height-link-small: calc( var(--td-font-size-link-small) + var(--td-line-height-common) );
--td-line-height-link-medium: calc( var(--td-font-size-link-medium) + var(--td-line-height-common) );
--td-line-height-link-large: calc( var(--td-font-size-link-large) + var(--td-line-height-common) );
--td-line-height-mark-small: calc( var(--td-font-size-mark-small) + var(--td-line-height-common) );
--td-line-height-mark-medium: calc( var(--td-font-size-mark-medium) + var(--td-line-height-common) );
--td-line-height-body-small: calc( var(--td-font-size-body-small) + var(--td-line-height-common) );
--td-line-height-body-medium: calc( var(--td-font-size-body-medium) + var(--td-line-height-common) );
--td-line-height-body-large: calc( var(--td-font-size-body-large) + var(--td-line-height-common) );
--td-line-height-title-small: calc( var(--td-font-size-title-small) + var(--td-line-height-common) );
--td-line-height-title-medium: calc( var(--td-font-size-title-medium) + var(--td-line-height-common) );
--td-line-height-title-large: calc( var(--td-font-size-title-medium) + var(--td-line-height-common) );
--td-line-height-headline-small: calc( var(--td-font-size-headline-small) + var(--td-line-height-common) );
--td-line-height-headline-medium: calc( var(--td-font-size-headline-medium) + var(--td-line-height-common) );
--td-line-height-headline-large: calc( var(--td-font-size-headline-large) + var(--td-line-height-common) );
--td-line-height-display-medium: calc( var(--td-font-size-display-medium) + var(--td-line-height-common) );
--td-line-height-display-large: calc( var(--td-font-size-display-large) + var(--td-line-height-common) );
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
--td-font-title-small: var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
--td-font-title-medium: var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
--td-font-title-large: var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
--td-font-headline-small: var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
/* 字体颜色 */
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
/* end 字体配置 */ /* 圆角配置 */
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
/* end 圆角配置 *//* 阴影配置 */
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08);
/* end 阴影配置 *//* 尺寸配置 */
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
/* end 尺寸配置 */
}
:root[theme-mode="dark"] {
--brand-main: var(--td-brand-color-6);
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-brand-color-1: #0894fa20;
--td-brand-color-2: #00325c;
--td-brand-color-3: #004881;
--td-brand-color-4: #0060a8;
--td-brand-color-5: #007ad3;
--td-brand-color-6: #0894fa;
--td-brand-color-7: #6bb2ff;
--td-brand-color-8: #a3ccff;
--td-brand-color-9: #d1e4ff;
--td-brand-color-10: #eef4ff;
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e7e7e7;
--td-gray-color-4: #dcdcdc;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e7e7e7;
--td-gray-color-4: #dcdcdc;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-secondarycomponent: var(--td-gray-color-10);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-9);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-8);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
/* 圆角配置 */
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
/* end 圆角配置 *//* 阴影配置 */
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08);
/* end 阴影配置 *//* 尺寸配置 */
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
/* end 尺寸配置 */
}

9766
src/views/AchievementView/AchievementView.vue

File diff suppressed because it is too large

226
src/views/AchievementView/components/ImageBar.vue

@ -0,0 +1,226 @@
<script setup>
import { OverlayScrollbars } from 'overlayscrollbars';
import { computed } from 'vue';
const props = defineProps({
list: {
type: Array,
default: () => [],
},
});
let scrollBar;
const vScroll = {
mounted: (el) => {
scrollBar = OverlayScrollbars(el, {
paddingAbsolute: true,
overflow: {
x: 'hidden',
y: 'scroll',
},
scrollbars: {
theme: 'os-theme-light',
autoHide: 'leave',
autoHideDelay: 300,
},
});
},
};
function onScrollTo(direction = 'down') {
const { scrollOffsetElement } = scrollBar.elements();
let top;
if (direction === 'down') {
top = scrollOffsetElement.scrollTop + 300;
}
if (direction === 'up') {
top = scrollOffsetElement.scrollTop - 300;
}
scrollOffsetElement.scrollTo({
behavior: 'smooth',
top,
});
}
const urlList = computed(() => props.list.map(({ url }) => url));
</script>
<template>
<div :class="s.root">
<div class="page-turning-left" @click="onScrollTo('up')">
<t-icon name="chevron-up" />
</div>
<div class="container" v-scroll>
<div class="custom-space">
<template v-for="(item, index) in list" :key="item.id">
<t-image-viewer
:images="urlList"
:default-index="index"
>
<template #trigger="{ open: onPreview }">
<div class="image-box">
<t-image
shape="round"
fit="cover"
:style="{ 'width': '20vh', height: '15vh' }"
:src="item.url"
error=""
>
<template #overlay-content>
<div class="overlay-container">
<div class="img-type">
{{ item.name }}
</div>
</div>
</template>
</t-image>
<div class="image-hover" @click="onPreview">
<span><t-icon name="browse" size="1.4em" /> 预览</span>
</div>
</div>
</template>
</t-image-viewer>
</template>
<template v-if="!list.length">
<t-image
shape="round"
fit="cover"
:style="{ 'width': '20vh', height: '15vh' }"
src=""
error=""
>
<template #overlay-content>
<div class="overlay-container">
<div class="img-type">
没有上传图片哦 ~
</div>
</div>
</template>
</t-image>
</template>
</div>
</div>
<div class="page-turning-right" @click="onScrollTo('down')">
<t-icon name="chevron-down" />
</div>
</div>
</template>
<style lang="less" module="s">
.root {
display: flex;
flex-direction: column;
align-items: center;
:global {
.page-turning-left {
background-color: fade(black, 35%);
padding: 0px 24px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
margin-bottom: 2px;
cursor: pointer;
&:hover {
background-color: fade(black, 55%);
}
}
.page-turning-right {
background-color: fade(black, 35%);
padding: 0px 24px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
margin-top: 2px;
cursor: pointer;
&:hover {
background-color: fade(black, 55%);
}
}
.container {
flex: 1;
background-color: fade(black, 35%);
padding: var(--td-comp-paddingTB-s);
border-radius: var(--td-radius-large);
overflow: hidden;
.custom-space {
display: flex;
flex-direction: column;
gap: var(--td-comp-margin-l);
.image-box {
flex-shrink: 0;
}
}
}
.overlay-container {
width: 100%;
height: 100%;
position: relative;
.img-quantity {
position: absolute;
top: 5px;
right: 5px;
background-color: fade(black, 60%);
padding: 2px 10px;
border-radius: 40px;
display: flex;
align-items: center;
font-size: 12px;
.text {
margin-left: 5px;
}
}
.img-type {
position: absolute;
bottom: 5px;
left: 5px;
background-color: fade(black, 60%);
padding: 2px 15px;
border-radius: 40px;
font-size: 12px;
}
}
.image-box {
//width: 40px;
//height: 60px;
display: inline-flex;
position: relative;
justify-content: center;
align-items: center;
border-radius: var(--td-radius-small);
overflow: hidden;
.image-hover {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
opacity: 0;
background-color: rgba(0, 0, 0, 0.6);
color: var(--td-text-color-anti);
line-height: 22px;
transition: 0.2s;
z-index: 1;
}
&:hover .image-hover {
opacity: 1;
cursor: pointer;
}
}
}
}
</style>

178
src/views/AchievementView/components/MediaManage.vue

@ -0,0 +1,178 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { REQUEST_UPLOAD_FILE } from '@/config/urls';
import auth from '@/utils/auth';
// import { storeToRefs } from 'pinia';
import { useMediaStore } from '@/stores';
import { MessagePlugin } from 'tdesign-vue-next';
import eventBus from '@/utils/eventBus';
const mediaStore = useMediaStore();
const { getMediaList, createMedia, deleteMedia } = mediaStore;
const sortieId = ref();
const visible = ref(false);
const files = ref([]);
function init() {
if (!sortieId.value) return;
getMediaList(sortieId.value).then(({ data }) => {
(data || []).forEach(item => {
const temp = item;
temp.status = 'success';
temp.raw = {};
});
files.value = [...(data || []), ...files.value];
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
const mediaList = ref([]);
function loadList() {
if (!sortieId.value) return;
getMediaList(sortieId.value).then(({ data }) => {
mediaList.value = [...(data || [])];
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
function onCancel() {
files.value = [];
mediaList.value = [];
}
const subject = ref('image');
const ABRIDGE_NAME = [10, 7];
const formatResponse = (res) => {
if (!res) {
return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' };
}
return res;
};
function beforeUpload(UploadFile) {
const { type = 'image' } = UploadFile || {};
if (type.includes('image')) {
subject.value = 'image';
}
if (type.includes('video')) {
subject.value = 'video';
}
return true;
}
const handleSuccess = ({ currentFiles = [], response = [] } = {}) => {
console.log('aaa', currentFiles, response);
const medias = currentFiles.map(({ name, size, type, status, response: { code, data } }) => {
if (status !== 'success') return null;
if (code !== 200) return null;
let typeInt;
if (type.includes('image')) {
typeInt = 1;
}
if (type.includes('video')) {
typeInt = 2;
}
return {
name,
size,
type: typeInt,
url: data,
};
}).filter(Boolean);
// const { name, size, type } = currentFiles[0] || {};
// const { code, data } = response[0] || {};
// if (code !== 200) return;
// let typeInt;
// if (type.includes('image')) {
// typeInt = 1;
// }
// if (type.includes('video')) {
// typeInt = 2;
// }
const formData = {
id: sortieId.value,
medias,
};
createMedia(formData).then(() => {
if (!visible.value) return;
loadList();
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
};
const handleRemove = ({ file } = {}) => {
if (file.status !== 'success') return;
let mediaId;
if (file.id) {
mediaId = file.id;
} else {
const { response: { data } } = file;
const media = mediaList.value.find(({ url }) => url === data);
mediaId = media?.id;
}
if (!mediaId) return;
deleteMedia(mediaId).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
};
onMounted(() => {
eventBus.on('show-media-manage', (row) => {
if (!row.id) return;
sortieId.value = row.id;
init();
visible.value = true;
});
});
onUnmounted(() => {
eventBus.off('show-media-manage');
});
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
width="fit-content"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">媒体管理</div>
</template>
<t-upload
v-model="files"
placeholder="支持上传图片、视频文件"
:action="REQUEST_UPLOAD_FILE(subject)"
:headers="{
Authorization: `Bearer ${auth.getToken()}`,
}"
theme="file-flow"
multiple
:abridge-name="ABRIDGE_NAME"
auto-upload
show-thumbnail
allow-upload-duplicate-file
:format-response="formatResponse"
:before-upload="beforeUpload"
@success="handleSuccess"
@remove="handleRemove"
/>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
}
</style>

173
src/views/AchievementView/components/SortieDetail.vue

@ -0,0 +1,173 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
// import { REQUEST_UPLOAD_FILE } from '@/config/urls';
// import auth from '@/utils/auth';
// import { storeToRefs } from 'pinia';
import { useMediaStore } from '@/stores';
import { MessagePlugin } from 'tdesign-vue-next';
import eventBus from '@/utils/eventBus';
import ImageBar from '@/views/SortieView/components/ImageBar.vue';
import VideoPlayer from '@/components/VideoPlayer.vue';
const mediaStore = useMediaStore();
const { getMediaList } = mediaStore;
const sortieId = ref();
const visible = ref(false);
// const files = ref([]);
// function init() {
// if (!sortieId.value) return;
// getMediaList(sortieId.value).then(({ data }) => {
// // data.forEach(item => {
// // const temp = item;
// // temp.status = 'success';
// // temp.raw = {};
// // });
// // files.value = [...data, ...files.value];
// }).catch(({ message }) => {
// if (message) MessagePlugin.error(message);
// });
// }
const mediaList = ref([]);
const imageList = computed(() => mediaList.value.filter(({ type }) => type === 1));
const videoList = computed(() => mediaList.value.filter(({ type }) => type === 2));
function loadList() {
if (!sortieId.value) return;
getMediaList(sortieId.value).then(({ data = [] } = {}) => {
mediaList.value = [...(data || [])];
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
function onCancel() {
mediaList.value = [];
}
// const subject = ref('image');
// const ABRIDGE_NAME = [10, 7];
//
// const formatResponse = (res) => {
// if (!res) {
// return { status: 'fail', error: '' };
// }
// return res;
// };
//
// function beforeUpload(UploadFile) {
// const { type = 'image' } = UploadFile || {};
// if (type.includes('image')) {
// subject.value = 'image';
// }
// if (type.includes('video')) {
// subject.value = 'video';
// }
// return true;
// }
// const handleSuccess = ({ currentFiles = [], response = [] } = {}) => {
// const { name, size, type } = currentFiles[0] || {};
// const { code, data } = response[0] || {};
// if (code !== 200) return;
// let typeInt;
// if (type.includes('image')) {
// typeInt = 1;
// }
// if (type.includes('video')) {
// typeInt = 2;
// }
// const formData = {
// id: sortieId.value,
// medias: [{
// name,
// size,
// type: typeInt,
// url: data,
// }],
// };
// createMedia(formData).then(() => {
// loadList();
// }).catch(({ message }) => {
// if (message) MessagePlugin.error(message);
// });
// };
// const handleRemove = ({ file } = {}) => {
// if (file.status !== 'success') return;
// let mediaId;
//
// if (file.id) {
// mediaId = file.id;
// } else {
// const { response: { data } } = file;
// const media = mediaList.value.find(({ url }) => url === data);
// mediaId = media?.id;
// }
//
// if (!mediaId) return;
// deleteMedia(mediaId).catch(({ message }) => {
// if (message) MessagePlugin.error(message);
// });
// };
onMounted(() => {
eventBus.on('show-sortie-detail', (row) => {
if (!row.id) return;
sortieId.value = row.id;
loadList();
visible.value = true;
});
});
onUnmounted(() => {
eventBus.off('show-sortie-detail');
});
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
mode="full-screen"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">查看媒体</div>
</template>
<div class="container">
<ImageBar :list="imageList" class="image-bar" />
<VideoPlayer :list="videoList" class="video-player" />
</div>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
:global {
.container {
height: 100%;
display: flex;
.video-player {
flex: 1;
margin-left: var(--td-comp-margin-s);
margin-top: var(--td-comp-margin-s);
border-radius: var(--td-radius-medium);
}
}
.t-dialog__position_fullscreen {
height: 100%;
}
}
}
</style>

147
src/views/DeviceView/DeviceView.vue

@ -0,0 +1,147 @@
<script setup>
import BasePanel from '@/components/BasePanel.vue';
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useDeviceStore } from '@/stores';
import { MessagePlugin } from 'tdesign-vue-next';
import DeviceEditor from '@/views/DeviceView/components/DeviceEditor.vue';
import eventBus from '@/utils/eventBus';
import { formatTime, popularTime } from '@/utils/helpers';
const deviceStore = useDeviceStore();
const { deviceList, deviceQueries, deviceExtra } = storeToRefs(deviceStore);
const { getDeviceList, updateDeviceState, deleteDevice } = deviceStore;
function loadList(queries = {}) {
getDeviceList(queries).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
loadList({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined });
function onPageChange({ current, pageSize }) {
loadList({ page: current, pageSize });
}
const search = ref();
function onSearchList() {
loadList({ search: search.value });
}
function onResetList() {
search.value = undefined;
onSearchList();
}
const typeLabel = { 1: '抛投无人机', 2: '巡检无人机' };
const columns = [
{ colKey: 'serial-number', title: '序列号', ellipsis: true, width: 100 },
{ colKey: 'sn', title: '飞控ID', ellipsis: true },
{ colKey: 'manufacturerName', title: '制造商', ellipsis: true },
{ colKey: 'type', title: '机型', ellipsis: true, cell: (_, { row }) => (typeLabel[row.type] || '-') },
{ colKey: 'onLine', title: '是否在线', ellipsis: true },
{ colKey: 'locked', title: '锁定状态', ellipsis: true },
{ colKey: 'workTime', title: '工作时长', ellipsis: true, cell: (_, { row }) => popularTime(row.workTime) || '0s' },
{ colKey: 'createdAt', title: '添加时间', ellipsis: true, cell: (_, { row }) => formatTime(row.createdAt * 1000), width: 200 },
{ colKey: 'operation', title: '操作', width: '300px' },
];
function onShowEditor(row = {}) {
eventBus.emit('show-device-editor', row);
}
const pagination = computed(() => ({
current: deviceQueries.value.page,
pageSize: deviceQueries.value.pageSize,
total: deviceExtra.value.total,
}));
function onChangeState(row = {}) {
updateDeviceState(row, { refreshList: true }).then(() => {
MessagePlugin.success('更改成功');
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
function onDeleteManufacturer(row = {}) {
deleteDevice(row.id, { refreshList: true }).then(() => {
MessagePlugin.success('成功删除');
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
</script>
<template>
<BasePanel>
<template #header>
<t-space>
<div class="vertical-line" />
<div class="header-title">设备管理</div>
</t-space>
</template>
<template #header-extra>
<t-space>
<t-button @click="onShowEditor">录入设备</t-button>
</t-space>
</template>
<template #top-bar>
<t-space align="center">
<div>关键字查询</div>
<t-input v-model="search" clearable />
<t-button @click="onSearchList">查询</t-button>
<t-button theme="default" @click="onResetList">重置</t-button>
</t-space>
</template>
<t-table
class="table-custom"
row-key="id"
:data="deviceList"
:columns="columns"
cell-empty-content="-"
:pagination="pagination"
@page-change="onPageChange"
>
<template #onLine="{ row }">
<t-tag theme="success" v-if="row.onLine">在线</t-tag>
<t-tag theme="danger" v-else>离线</t-tag>
</template>
<template #locked="{ row }">
<t-tag theme="success" v-if="!row.locked">未锁定</t-tag>
<t-tag theme="danger" v-else>已锁定</t-tag>
</template>
<template #operation="{ row }">
<t-space align="center">
<t-popconfirm :content="`确认${!row.locked ? '锁定' : '解锁'}吗?`" @confirm="onChangeState(row)">
<t-button :theme="!row.locked ? 'danger' : 'success'">{{ !row.locked ? '锁定' : '解锁' }}</t-button>
</t-popconfirm>
<t-popconfirm :content="`确认删除吗?`" @confirm="onDeleteManufacturer(row)">
<t-button theme="danger">删除</t-button>
</t-popconfirm>
<t-button @click="onShowEditor(row)">编辑</t-button>
</t-space>
</template>
</t-table>
</BasePanel>
<DeviceEditor />
</template>
<style lang="less" module="s">
.root {
//
}
.dropdown {
:global {
.t-dropdown__submenu ul {
margin: 0;
}
}
}
</style>

117
src/views/DeviceView/components/DeviceEditor.vue

@ -0,0 +1,117 @@
<script setup>
import { onMounted, onUnmounted, ref, computed } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { useDeviceStore } from '@/stores';
import eventBus from '@/utils/eventBus';
// import ImageUploader from '@/components/ImageUploader.vue';
import ManufacturerSelector from '@/components/ManufacturerSelector.vue';
import { update } from '@/utils/helpers';
const visible = ref(false);
const deviceStore = useDeviceStore();
const { createDevice, updateDevice } = deviceStore;
const form = ref();
const formData = ref({
id: undefined,
manufacturerId: undefined,
sn: undefined,
type: 1,
});
const FORM_RULES = {
sn: [{ required: true, message: '请输入飞控ID' }],
manufacturerId: [{ required: true, message: '请选择制造商' }],
type: [{ required: true, message: '请选择' }],
};
function onCancel() {
visible.value = false;
form.value.reset();
}
function onSubmit({ validateResult }) {
if (validateResult === true) {
if (formData.value.id) {
updateDevice(formData.value, { refreshList: true }).then(() => {
MessagePlugin.success('更新成功');
onCancel();
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
return;
}
createDevice(formData.value, { refreshList: true }).then(() => {
MessagePlugin.success('创建成功');
onCancel();
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
}
onMounted(() => {
eventBus.on('show-device-editor', (row = {}) => {
if (row.id) {
formData.value = update(formData.value, row);
}
visible.value = true;
});
});
onUnmounted(() => {
eventBus.off('show-device-editor');
});
const options = [{ value: 1, label: '抛投无人机' }, { value: 2, label: '巡检无人机' }];
const isEdit = computed(() => !!formData.value?.id);
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
:close-btn="false"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">{{ formData.id ? '更新' : '录入' }}设备</div>
</template>
<t-form
ref="form"
:data="formData"
:rules="FORM_RULES"
colon
@submit="onSubmit"
>
<t-form-item name="id" v-show="false" />
<t-form-item label="飞控ID" name="sn">
<t-input v-model="formData.sn" :disabled="isEdit" />
</t-form-item>
<t-form-item label="制造商" name="manufacturerId">
<ManufacturerSelector v-model="formData.manufacturerId" />
</t-form-item>
<t-form-item label="机型" name="type">
<t-select v-model="formData.type" :options="options" placeholder="请选择机型" />
</t-form-item>
<t-form-item>
<t-space>
<t-button theme="primary" type="submit">提交</t-button>
<t-button theme="default" @click="onCancel">取消</t-button>
</t-space>
</t-form-item>
</t-form>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
}
</style>

82
src/views/ExampleView/ExampleView.vue

@ -1,8 +1,88 @@
<script setup>
import BasePanel from '@/components/BasePanel.vue';
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
import HlsPlugin from 'xgplayer-hls';
import { onMounted, ref } from 'vue';
import MultipleFilesUploader from '@/components/MultipleFilesUploader.vue';
// import mp4 from '@/assets/xgplayer-demo.mp4';
// let player = new Player({
// id: 'mse',
// url: '//abc.com/**/*.mp4',
// height: '100%',
// width: '100%',
// });
const playerRef = ref();
onMounted(() => {
const options = {
id: 'player',
url: 'http://pull.jiagutech.com/live/test2.m3u8', // HLS
// url: mp4, // HLS
type: 'hls',
// fluid: true,
autoplayMuted: true,
autoplay: true,
plugins: [HlsPlugin],
lang: 'zh',
// preset: ['default'],
width: 500,
height: 500,
};
playerRef.value = new Player(options);
});
const files = ref([]);
setTimeout(() => {
files.value.push({
raw: {},
// lastModified: 1636543695727,
name: '测试2.jpg',
size: 50353,
// type: 'image/jpeg',
// percent: 100,
status: 'success',
type: 1,
// uploadTime: '2023-12-15',
// response: {
// url: 'https://tdesign.gtimg.com/demo/demo-image-1.png',
// XMLHttpRequest: {},
// },
url: 'https://tdesign.gtimg.com/demo/demo-image-1.png',
});
});
// const files = ref([{
// raw: {},
// // lastModified: 1636543695727,
// name: '2.jpg',
// size: 50353,
// // type: 'image/jpeg',
// // percent: 100,
// status: 'success',
// // uploadTime: '2023-12-15',
// // response: {
// // url: 'https://tdesign.gtimg.com/demo/demo-image-1.png',
// // XMLHttpRequest: {},
// // },
// url: 'https://tdesign.gtimg.com/demo/demo-image-1.png',
// }]);
</script>
<template>
<div v-if="false" :class="s.root">ExampleView</div>
<BasePanel>
<div>
<div id="player" />
</div>
<div>
<MultipleFilesUploader v-model="files" />
</div>
</BasePanel>
</template>
<style lang="less" module="s">

16
src/views/ManufacturerView/ManufacturerView.vue

@ -18,7 +18,7 @@
});
}
loadList();
loadList({ page: 1, pageSize: 10, all: undefined, search: undefined });
function onPageChange({ current, pageSize }) {
loadList({ page: current, pageSize });
@ -29,6 +29,11 @@
loadList({ search: search.value });
}
function onResetList() {
search.value = undefined;
onSearchList();
}
const columns = [
{ colKey: 'manufacturerName', title: '企业/组织名称', ellipsis: true },
{ colKey: 'contactName', title: '联系人名称', ellipsis: true },
@ -70,7 +75,12 @@
<template>
<BasePanel>
<template #header>制造商管理</template>
<template #header>
<t-space>
<div class="vertical-line" />
<div class="header-title">制造商管理</div>
</t-space>
</template>
<template #header-extra>
<t-space>
<t-button @click="onShowEditor">新增制造商</t-button>
@ -82,10 +92,12 @@
<div>关键字查询</div>
<t-input v-model="search" clearable />
<t-button @click="onSearchList">查询</t-button>
<t-button theme="default" @click="onResetList">重置</t-button>
</t-space>
</template>
<t-table
class="table-custom"
row-key="id"
:data="manufacturerList"
:columns="columns"

4
src/views/ManufacturerView/components/ManufacturerEditor.vue

@ -131,11 +131,11 @@
</t-form-item>
<t-form-item label="营业执照扫描件" name="businessLicense">
<ImageUploader subject="device_pic" v-model="formData.businessLicense" />
<ImageUploader subject="businessLicense" v-model="formData.businessLicense" />
</t-form-item>
<t-form-item label="公司LOGO" name="companyLogo">
<ImageUploader subject="device_pic" v-model="formData.companyLogo" />
<ImageUploader subject="logo" v-model="formData.companyLogo" />
</t-form-item>
<t-form-item>

1674
src/views/MonitorView/MonitorView.vue

File diff suppressed because it is too large

105
src/views/MonitorView/components/DeviceStatBox.vue

@ -0,0 +1,105 @@
<script setup>
// import { mapState, mapGetters, mapActions } from 'vuex';
// import _throttle from 'lodash.throttle';
// import { getAddressLabel } from '../../../utils/cascadeDistrict';
// import deviceTypes from '../../../config/deviceTypes';
// import eventBus from '@/utils/eventBus';
import { onMounted, onUnmounted } from 'vue';
// const detail = ref({});
// const dynamicInfo = computed(() => {
// // eslint-disable-next-line no-shadow
// const { dynamicInfo } = detail.value;
// return dynamicInfo || {};
// });
// const baseInfo = computed(() => {
// // eslint-disable-next-line no-shadow
// const { baseInfo } = detail.value;
// return baseInfo || {};
// });
// const isVisible = ref(false);
onMounted(() => {
//
});
onUnmounted(() => {
//
});
</script>
<template>
<div :class="s.root">
<t-space align="center">
<t-divider layout="vertical" />
<t-statistic title="无人机总数" :value="123456" prefix="$" unit="个" color="blue">
<template #prefix>
<t-icon name="data-base" />
</template>
</t-statistic>
<t-divider layout="vertical" />
<t-statistic title="在线数量" :value="123456" prefix="$" unit="个" color="orange">
<template #prefix>
<t-icon name="gps" />
</template>
</t-statistic>
<t-divider layout="vertical" />
<t-statistic title="总架次时长" :value="123456" prefix="$" unit="h" color="green">
<template #prefix>
<t-icon name="time" />
</template>
</t-statistic>
<t-divider layout="vertical" />
<t-statistic title="总架次数" :value="123456" prefix="$" unit="次" color="red">
<template #prefix>
<t-icon name="analytics" />
</template>
</t-statistic>
<t-divider layout="vertical" />
</t-space>
</div>
</template>
<style lang="less" module="s">
.root {
//display: flex;
//justify-content: flex-end;
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
//width: 35vh;
//height: 200px;
background-color: fade(#000, 60%);
//border-radius: 4px;
//overflow: hidden;
//transition: right .2s ease;
//pointer-events: none;
box-sizing: border-box;
border: 10px solid transparent;
border-image: url("../../../assets/bg.svg");
border-image-repeat: stretch;
border-image-slice: 10;
box-shadow: 0 0 10px 0 var(--td-brand-color);
//&:global::after {
// content: '';
// position: absolute;
// top: 0;
// bottom: 0;
// right: 0;
// left: 0;
// //background: url("../../../assets/bg.svg") no-repeat scroll center bottom transparent;
//}
:global {
//
.t-statistic-title {
color: fade(#eee, 60%);
}
}
}
</style>

303
src/views/MonitorView/components/DroneDetail.vue

@ -0,0 +1,303 @@
<script setup>
// import { mapState, mapGetters, mapActions } from 'vuex';
// import _throttle from 'lodash.throttle';
// import { getAddressLabel } from '../../../utils/cascadeDistrict';
// import deviceTypes from '../../../config/deviceTypes';
import eventBus from '@/utils/eventBus';
import { computed, onMounted, onUnmounted, ref } from 'vue';
const detail = ref({});
const dynamicInfo = computed(() => {
// eslint-disable-next-line no-shadow
const { dynamicInfo } = detail.value;
return dynamicInfo || {};
});
const baseInfo = computed(() => {
// eslint-disable-next-line no-shadow
const { baseInfo } = detail.value;
return baseInfo || {};
});
const isVisible = ref(false);
onMounted(() => {
eventBus.on('show-real-time-plane', (data = {}) => {
detail.value = data;
isVisible.value = true;
console.log(isVisible.value);
});
eventBus.on('hide-all-panels', () => {
isVisible.value = false;
console.log(isVisible.value);
});
});
onUnmounted(() => {
eventBus.off('show-real-time-plane');
eventBus.off('hide-all-panels');
});
</script>
<template>
<div :class="s.root" v-if="isVisible">
<div class="columns">
<div class="column custom1" v-if="dynamicInfo.online">
<div class="columns is-mobile is-gapless is-marginless ui-own-title">
<div class="column">实时动态信息</div>
<div class="column is-narrow" />
</div>
<table class="table" v-if="baseInfo.recorderType !== 2">
<colgroup>
<col width="150">
<col width="200">
</colgroup>
<tr>
<th>农机状态</th>
<td>{{ dynamicInfo.landStatus ? '-' : '作业中' }}</td>
</tr>
<tr>
<th>当前位置</th>
<td />
</tr>
<!-- <tr>-->
<!-- <th>卫星数</th>-->
<!-- <td>{{ dynamicInfo.satelliteCount }}</td>-->
<!-- </tr>-->
<tr>
<th>定位精度</th>
<td>{{ }}</td>
</tr>
<tr>
<th>经纬度</th>
<!-- <td>{{ dynamicInfo.lng }}, {{ dynamicInfo.lat }}</td>-->
</tr>
<!-- <tr>-->
<!-- <th>海拔高度</th>-->
<!-- <td>{{ dynamicInfo.altitude }}m</td>-->
<!-- </tr>-->
<tr>
<th>对地高度</th>
<td>{{ dynamicInfo.height }}m</td>
</tr>
<tr>
<th>水平速度</th>
<td>{{ dynamicInfo.xspeed }}m/s</td>
</tr>
<!-- <tr>-->
<!-- <th>垂直速度</th>-->
<!-- <td>{{ dynamicInfo.yspeed }}m/s</td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <th>航向角</th>-->
<!-- <td>{{ dynamicInfo.mha }}°</td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <th>俯仰角</th>-->
<!-- <td>{{ dynamicInfo.pa }}°</td>-->
<!-- </tr>-->
<!-- <tr>-->
<!-- <th>横滚角</th>-->
<!-- <td>{{ dynamicInfo.ra }}°</td>-->
<!-- </tr>-->
<tr>
<th>喷洒流速</th>
<td>{{ dynamicInfo.flowSpeed }}L/min</td>
</tr>
</table>
</div>
<div class="column custom2">
<div class="columns ui-own-title">
<div class="column">基本信息</div>
<div class="column" v-if="false">
<b>违章记录</b>
</div>
</div>
<table class="table">
<colgroup>
<col width="120">
<col width="200">
</colgroup>
<tr>
<th>农机ID</th>
<td>{{ baseInfo.droneId }}</td>
</tr>
<tr>
<th>农机类型</th>
<td>{{ baseInfo.deviceType }}</td>
</tr>
<tr>
<th>制造商</th>
<td>{{ baseInfo.zzAccountName }}</td>
</tr>
<tr>
<th>制造商</th>
<td>{{ baseInfo.zzAccountName }}</td>
</tr>
<tr>
<th>制造商</th>
<td>{{ baseInfo.zzAccountName }}</td>
</tr>
<tr v-if="false">
<th>机型</th>
<td>{{ baseInfo.modelName }}</td>
</tr>
<tr v-if="false">
<th>运营人</th>
<td>{{ baseInfo.owner }}</td>
</tr>
<tr v-if="false">
<th>实名登记标识</th>
<td>{{ baseInfo.regMark }}</td>
</tr>
</table>
<div class="columns ui-own-title" v-if="false">
<div class="column">动态统计信息</div>
<div class="column">
<b>历史作业</b>
</div>
</div>
<table class="table" v-if="false">
<colgroup>
<col width="120">
<col width="150">
</colgroup>
<tr>
<th>总作业时长</th>
<td>{{ dynamicInfo.flyTotalDuration }}h</td>
</tr>
<tr>
<th>总作业里程</th>
<td>{{ dynamicInfo.flyTotalMileage }}km</td>
</tr>
<tr>
<th>总作业亩数</th>
<td>10000</td>
</tr>
<tr>
<th>飞控总亩数</th>
<td>10000</td>
</tr>
<tr>
<th>总喷洒量</th>
<td>{{ dynamicInfo.sparyTotalAmount }}L</td>
</tr>
<tr>
<th>总作业架次</th>
<td>{{ dynamicInfo.flySeqCount }}</td>
</tr>
</table>
</div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
//display: flex;
//justify-content: flex-end;
position: absolute;
top: 106px;
right: 10px;
z-index: 1;
width: 35vh;
//height: 200px;
//background-color: fade(#000, 20%);
//border-radius: 4px;
//overflow: hidden;
//transition: right .2s ease;
//pointer-events: none;
//background-image: url("../../../assets/map-tan-1.png");
//background-repeat: no-repeat;
//background-size: contain;
//background-size: 100% 100%;
//box-sizing: border-box;
//border: 10px solid transparent;
//border-image: url("../../../assets/bg.svg");
//border-image-repeat: stretch;
//border-image-slice: 10;
//
//box-shadow: 0 0 10px 0 var(--td-brand-color);
//&:global::after {
// content: '';
// position: absolute;
// top: 0;
// bottom: 0;
// right: 0;
// left: 0;
// //background: url("../../../assets/bg.svg") no-repeat scroll center bottom transparent;
//}
:global {
//.demo-item {
// width: 100px;
// height: 100px;
// background-color: red;
//}
.columns {
.custom1 {
min-height: 232px;
background-image: url("../../../assets/map-tan-1-1.png");
background-repeat: no-repeat;
background-size: contain;
background-size: 100% 100%;
}
.custom2 {
min-height: 246px;
margin-top: -1px;
background-image: url("../../../assets/map-tan-1-2.png");
background-repeat: no-repeat;
background-size: contain;
background-size: 100% 100%;
}
}
th {
color: #27e7dc;
}
td {
color: white;
}
.column {
pointer-events: auto;
}
.ui-own-title {
//background-color: var(--td-brand-color);
color: var(--td-text-color-anti);
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-xl) 0 var(--td-comp-paddingLR-xl);
line-height: 1;
font-weight: bold;
font-size: var(--td-font-size-title-medium);
//.column:first-child:before {
// content: '';
// display: inline-block;
// width: 3px;
// height: 100%;
// background-color: #fff;
// margin-right: 5px;
// vertical-align: -3px;
//}
//b {
// font-weight: normal;
// border-bottom: 1px solid;
// cursor: pointer;
//}
}
.table {
margin-bottom: 0;
font-size: 14px;
padding: 10px;
}
}
}
</style>

1
src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue

@ -173,6 +173,7 @@
noFlyZoneRenderer.fitShape(formData.value.id);
noFlyZoneRenderer.clear();
noFlyZoneEditor.loadDataSource({ id: formData.value.id, points: formData.value.orbit });
showOperationTips(2);
} else {
noFlyZoneCreator.start();
showOperationTips(1);

21
src/views/NoFlyZoneView/NoFlyZoneView.vue

@ -70,7 +70,7 @@
});
}
loadList();
loadList({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined });
function onPageChange({ current, pageSize }) {
loadList({ page: current, pageSize });
@ -85,6 +85,11 @@
loadList({ search: search.value });
}
function onResetList() {
search.value = undefined;
onSearchList();
}
const columns = [
{ colKey: 'detailAddress', title: '禁飞区名称', ellipsis: true, width: '300px' },
{
@ -164,13 +169,19 @@
}
onUnmounted(() => {
noFlyZoneRenderer.clear();
// noFlyZoneRenderer.clear();
noFlyZoneRenderer.destroy();
});
</script>
<template>
<BasePanel :width="width" :expand-ability="expandAbility">
<template #header>禁飞区管理</template>
<BasePanel :width="width" scrollbar>
<template #header>
<t-space>
<div class="vertical-line" />
<div class="header-title">禁飞区管理</div>
</t-space>
</template>
<template #header-extra>
<t-space>
<t-button @click="onNavTo('NoFlyZoneCreateView')">新增禁飞区</t-button>
@ -183,10 +194,12 @@
<div>关键字查询</div>
<t-input v-model="search" clearable />
<t-button @click="onSearchList">查询</t-button>
<t-button theme="default" @click="onResetList">重置</t-button>
</t-space>
</template>
<t-table
class="table-custom"
row-key="id"
:data="noFlyZoneList"
:columns="columns"

11
src/views/RegulatorView/RegulatorView.vue

@ -17,7 +17,7 @@
});
}
loadList();
loadList({ page: 1, pageSize: 10, all: undefined, search: undefined });
function onPageChange({ current, pageSize }) {
loadList({ page: current, pageSize });
@ -25,6 +25,7 @@
const columns = [
{ colKey: 'serial-number', title: '序列号', ellipsis: true },
{ colKey: 'username', title: '用户名', ellipsis: true },
// { colKey: 'contactName', title: '', ellipsis: true },
{ colKey: 'phone', title: '手机号(账号)', ellipsis: true },
];
@ -43,7 +44,12 @@
<template>
<BasePanel>
<template #header>监管者管理</template>
<template #header>
<t-space>
<div class="vertical-line" />
<div class="header-title">监管者管理</div>
</t-space>
</template>
<template #header-extra>
<t-space>
<t-button @click="onShowEditor">新增监管者</t-button>
@ -59,6 +65,7 @@
</template>
<t-table
class="table-custom"
row-key="id"
:data="regulatorList"
:columns="columns"

7
src/views/RegulatorView/components/RegulatorEditor.vue

@ -11,10 +11,12 @@
const form = ref();
const formData = ref({
username: undefined,
phone: undefined,
password: undefined,
});
const FORM_RULES = {
username: [{ required: true, message: '请输入用户名' }],
phone: [{ required: true, message: '请输入手机号' }],
password: [{ required: true, message: '请输入密码' }],
};
@ -61,10 +63,13 @@
ref="form"
:data="formData"
:rules="FORM_RULES"
label-width="150px"
colon
@submit="onSubmit"
>
<t-form-item label="用户名" name="username">
<t-input v-model="formData.username" />
</t-form-item>
<t-form-item label="手机号" name="phone" help="创建后的登录账号">
<t-input v-model="formData.phone" />
</t-form-item>

9731
src/views/SortieView/SortieView.vue

File diff suppressed because it is too large

229
src/views/SortieView/components/ImageBar.vue

@ -0,0 +1,229 @@
<script setup>
import { OverlayScrollbars } from 'overlayscrollbars';
import { computed } from 'vue';
const props = defineProps({
list: {
type: Array,
default: () => [],
},
});
// let scrollBar;
const vScroll = {
mounted: (el) => {
// scrollBar = OverlayScrollbars(el, {
OverlayScrollbars(el, {
paddingAbsolute: true,
overflow: {
x: 'hidden',
y: 'scroll',
},
scrollbars: {
theme: 'os-theme-light',
autoHide: 'leave',
autoHideDelay: 300,
},
});
},
};
// function onScrollTo(direction = 'down') {
// const { scrollOffsetElement } = scrollBar.elements();
// let top;
// if (direction === 'down') {
// top = scrollOffsetElement.scrollTop + 300;
// }
// if (direction === 'up') {
// top = scrollOffsetElement.scrollTop - 300;
// }
// scrollOffsetElement.scrollTo({
// behavior: 'smooth',
// top,
// });
// }
const urlList = computed(() => props.list.map(({ url }) => url));
</script>
<template>
<div :class="s.root">
<!-- <div class="page-turning-left" @click="onScrollTo('up')">-->
<!-- <t-icon name="chevron-up" />-->
<!-- </div>-->
<div class="container" v-scroll>
<div class="custom-space">
<template v-for="(item, index) in list" :key="item.id">
<t-image-viewer
:images="urlList"
:default-index="index"
>
<template #trigger="{ open: onPreview }">
<div class="image-box">
<t-image
shape="round"
fit="cover"
:style="{ 'width': '20vh', height: '15vh' }"
:src="item.url"
error=""
>
<template #overlay-content>
<div class="overlay-container">
<div class="img-type">
{{ item.name }}
</div>
</div>
</template>
</t-image>
<div class="image-hover" @click="onPreview">
<span><t-icon name="browse" size="1.4em" /> 预览</span>
</div>
</div>
</template>
</t-image-viewer>
</template>
<template v-if="!list.length">
<t-image
shape="round"
fit="cover"
:style="{ 'width': '20vh', height: '15vh' }"
src=""
error=""
>
<template #overlay-content>
<div class="overlay-container">
<div class="img-type">
没有上传图片哦 ~
</div>
</div>
</template>
</t-image>
</template>
</div>
</div>
<!-- <div class="page-turning-right" @click="onScrollTo('down')">-->
<!-- <t-icon name="chevron-down" />-->
<!-- </div>-->
</div>
</template>
<style lang="less" module="s">
.root {
display: flex;
flex-direction: column;
align-items: center;
:global {
.page-turning-left {
background-color: fade(black, 35%);
padding: 0px 24px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
margin-bottom: 2px;
cursor: pointer;
&:hover {
background-color: fade(black, 55%);
}
}
.page-turning-right {
background-color: fade(black, 35%);
padding: 0px 24px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
margin-top: 2px;
cursor: pointer;
&:hover {
background-color: fade(black, 55%);
}
}
.container {
flex: 1;
//background-color: fade(black, 35%);
background-color: var(--td-component-border);
padding: var(--td-comp-paddingTB-s);
border-radius: var(--td-radius-large);
overflow: hidden;
.custom-space {
display: flex;
flex-direction: column;
gap: var(--td-comp-margin-l);
.image-box {
flex-shrink: 0;
}
}
}
.overlay-container {
width: 100%;
height: 100%;
position: relative;
.img-quantity {
position: absolute;
top: 5px;
right: 5px;
background-color: fade(black, 60%);
padding: 2px 10px;
border-radius: 40px;
display: flex;
align-items: center;
font-size: 12px;
.text {
margin-left: 5px;
}
}
.img-type {
position: absolute;
bottom: 5px;
left: 5px;
background-color: fade(black, 60%);
padding: 2px 15px;
border-radius: 40px;
font-size: 12px;
color: white;
}
}
.image-box {
//width: 40px;
//height: 60px;
display: inline-flex;
position: relative;
justify-content: center;
align-items: center;
border-radius: var(--td-radius-small);
overflow: hidden;
.image-hover {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
opacity: 0;
background-color: rgba(0, 0, 0, 0.6);
color: var(--td-text-color-anti);
line-height: 22px;
transition: 0.2s;
z-index: 1;
}
&:hover .image-hover {
opacity: 1;
cursor: pointer;
}
}
}
}
</style>

178
src/views/SortieView/components/MediaManage.vue

@ -0,0 +1,178 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { REQUEST_UPLOAD_FILE } from '@/config/urls';
import auth from '@/utils/auth';
// import { storeToRefs } from 'pinia';
import { useMediaStore } from '@/stores';
import { MessagePlugin } from 'tdesign-vue-next';
import eventBus from '@/utils/eventBus';
const mediaStore = useMediaStore();
const { getMediaList, createMedia, deleteMedia } = mediaStore;
const sortieId = ref();
const visible = ref(false);
const files = ref([]);
function init() {
if (!sortieId.value) return;
getMediaList(sortieId.value).then(({ data }) => {
(data || []).forEach(item => {
const temp = item;
temp.status = 'success';
temp.raw = {};
});
files.value = [...(data || []), ...files.value];
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
const mediaList = ref([]);
function loadList() {
if (!sortieId.value) return;
getMediaList(sortieId.value).then(({ data }) => {
mediaList.value = [...(data || [])];
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
function onCancel() {
files.value = [];
mediaList.value = [];
}
const subject = ref('image');
const ABRIDGE_NAME = [10, 7];
const formatResponse = (res) => {
if (!res) {
return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' };
}
return res;
};
function beforeUpload(UploadFile) {
const { type = 'image' } = UploadFile || {};
if (type.includes('image')) {
subject.value = 'image';
}
if (type.includes('video')) {
subject.value = 'video';
}
return true;
}
const handleSuccess = ({ currentFiles = [], response = [] } = {}) => {
console.log('aaa', currentFiles, response);
const medias = currentFiles.map(({ name, size, type, status, response: { code, data } }) => {
if (status !== 'success') return null;
if (code !== 200) return null;
let typeInt;
if (type.includes('image')) {
typeInt = 1;
}
if (type.includes('video')) {
typeInt = 2;
}
return {
name,
size,
type: typeInt,
url: data,
};
}).filter(Boolean);
// const { name, size, type } = currentFiles[0] || {};
// const { code, data } = response[0] || {};
// if (code !== 200) return;
// let typeInt;
// if (type.includes('image')) {
// typeInt = 1;
// }
// if (type.includes('video')) {
// typeInt = 2;
// }
const formData = {
id: sortieId.value,
medias,
};
createMedia(formData).then(() => {
if (!visible.value) return;
loadList();
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
};
const handleRemove = ({ file } = {}) => {
if (file.status !== 'success') return;
let mediaId;
if (file.id) {
mediaId = file.id;
} else {
const { response: { data } } = file;
const media = mediaList.value.find(({ url }) => url === data);
mediaId = media?.id;
}
if (!mediaId) return;
deleteMedia(mediaId).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
};
onMounted(() => {
eventBus.on('show-media-manage', (row) => {
if (!row.id) return;
sortieId.value = row.id;
init();
visible.value = true;
});
});
onUnmounted(() => {
eventBus.off('show-media-manage');
});
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
width="fit-content"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">媒体管理</div>
</template>
<t-upload
v-model="files"
placeholder="支持上传图片、视频文件"
:action="REQUEST_UPLOAD_FILE(subject)"
:headers="{
Authorization: `Bearer ${auth.getToken()}`,
}"
theme="file-flow"
multiple
:abridge-name="ABRIDGE_NAME"
auto-upload
show-thumbnail
allow-upload-duplicate-file
:format-response="formatResponse"
:before-upload="beforeUpload"
@success="handleSuccess"
@remove="handleRemove"
/>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
}
</style>

173
src/views/SortieView/components/SortieDetail.vue

@ -0,0 +1,173 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
// import { REQUEST_UPLOAD_FILE } from '@/config/urls';
// import auth from '@/utils/auth';
// import { storeToRefs } from 'pinia';
import { useMediaStore } from '@/stores';
import { MessagePlugin } from 'tdesign-vue-next';
import eventBus from '@/utils/eventBus';
import ImageBar from '@/views/SortieView/components/ImageBar.vue';
import VideoPlayer from '@/components/VideoPlayer.vue';
const mediaStore = useMediaStore();
const { getMediaList } = mediaStore;
const sortieId = ref();
const visible = ref(false);
// const files = ref([]);
// function init() {
// if (!sortieId.value) return;
// getMediaList(sortieId.value).then(({ data }) => {
// // data.forEach(item => {
// // const temp = item;
// // temp.status = 'success';
// // temp.raw = {};
// // });
// // files.value = [...data, ...files.value];
// }).catch(({ message }) => {
// if (message) MessagePlugin.error(message);
// });
// }
const mediaList = ref([]);
const imageList = computed(() => mediaList.value.filter(({ type }) => type === 1));
const videoList = computed(() => mediaList.value.filter(({ type }) => type === 2));
function loadList() {
if (!sortieId.value) return;
getMediaList(sortieId.value).then(({ data = [] } = {}) => {
mediaList.value = [...(data || [])];
}).catch(({ message }) => {
if (message) MessagePlugin.error(message);
});
}
function onCancel() {
mediaList.value = [];
}
// const subject = ref('image');
// const ABRIDGE_NAME = [10, 7];
//
// const formatResponse = (res) => {
// if (!res) {
// return { status: 'fail', error: '' };
// }
// return res;
// };
//
// function beforeUpload(UploadFile) {
// const { type = 'image' } = UploadFile || {};
// if (type.includes('image')) {
// subject.value = 'image';
// }
// if (type.includes('video')) {
// subject.value = 'video';
// }
// return true;
// }
// const handleSuccess = ({ currentFiles = [], response = [] } = {}) => {
// const { name, size, type } = currentFiles[0] || {};
// const { code, data } = response[0] || {};
// if (code !== 200) return;
// let typeInt;
// if (type.includes('image')) {
// typeInt = 1;
// }
// if (type.includes('video')) {
// typeInt = 2;
// }
// const formData = {
// id: sortieId.value,
// medias: [{
// name,
// size,
// type: typeInt,
// url: data,
// }],
// };
// createMedia(formData).then(() => {
// loadList();
// }).catch(({ message }) => {
// if (message) MessagePlugin.error(message);
// });
// };
// const handleRemove = ({ file } = {}) => {
// if (file.status !== 'success') return;
// let mediaId;
//
// if (file.id) {
// mediaId = file.id;
// } else {
// const { response: { data } } = file;
// const media = mediaList.value.find(({ url }) => url === data);
// mediaId = media?.id;
// }
//
// if (!mediaId) return;
// deleteMedia(mediaId).catch(({ message }) => {
// if (message) MessagePlugin.error(message);
// });
// };
onMounted(() => {
eventBus.on('show-sortie-detail', (row) => {
if (!row.id) return;
sortieId.value = row.id;
loadList();
visible.value = true;
});
});
onUnmounted(() => {
eventBus.off('show-sortie-detail');
});
</script>
<template>
<t-dialog
:class="s.root"
v-model:visible="visible"
mode="full-screen"
:footer="null"
@closed="onCancel"
>
<template #header>
<div style="flex: 1; text-align: center;">查看媒体</div>
</template>
<div class="container">
<ImageBar :list="imageList" class="image-bar" />
<VideoPlayer :list="videoList" class="video-player" />
</div>
</t-dialog>
</template>
<style lang="less" module="s">
.root {
//
:global {
.container {
height: 100%;
display: flex;
.video-player {
flex: 1;
margin-left: var(--td-comp-margin-s);
//margin-top: var(--td-comp-margin-s);
border-radius: var(--td-radius-medium);
}
}
.t-dialog__position_fullscreen {
height: 100%;
}
}
}
</style>

6
src/views/common/useGlobalSettings.js

@ -6,9 +6,9 @@ import { createGlobalState, useStorage } from '@vueuse/core';
export const useGlobalSettings = createGlobalState(() => {
const settings = useStorage('settings', {
showMapHd: true,
showFieldFill: true,
showFieldName: true,
showNoFlyZone: true,
// showFieldFill: true,
// showFieldName: true,
});
const valueOf = () => toValue(settings);

19
vite.config.js

@ -62,17 +62,18 @@ export default defineConfig(({ mode }) => {
// esbuild: { loader: { '.js': '.jsx' } },
server: {
// host: true, // 监听所有地址
host: true, // 监听所有地址
proxy: {
// 使用 proxy 实例
// '/api': {
// // target: 'http://192.168.10.79:9999',
// // target: 'http://192.168.10.111:9900',
// // target: 'http://192.168.10.32:8102',
// // target: 'http://192.168.10.32:9999',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// },
'/api': {
// target: 'http://192.168.10.79:9998',
target: 'http://sgcloud-test.jiagutech.com/api',
// target: 'http://192.168.10.111:9900',
// target: 'http://192.168.10.32:8102',
// target: 'http://192.168.10.32:9999',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
// 案例
// // 字符串简写写法:http://localhost:5173/foo -> http://localhost:4567/foo
// '/foo': 'http://localhost:4567',

Loading…
Cancel
Save