Browse Source

【阶段】一阶段

main
xiaosi 4 days ago
parent
commit
cd320a8352
  1. 4
      .env.development
  2. 4
      .env.production
  3. 2
      .idea/misc.xml
  4. 4
      .idea/uav-edu-mp.iml
  5. 5
      components.d.ts
  6. 14
      out/production/uav-edu-mp/.editorconfig
  7. 6
      out/production/uav-edu-mp/.env.development
  8. 3
      out/production/uav-edu-mp/.env.production
  9. 1
      out/production/uav-edu-mp/.env.test
  10. 5
      out/production/uav-edu-mp/.eslintrc
  11. 8
      out/production/uav-edu-mp/.gitignore
  12. 8
      out/production/uav-edu-mp/.idea/.gitignore
  13. 7
      out/production/uav-edu-mp/.idea/inspectionProfiles/Project_Default.xml
  14. 6
      out/production/uav-edu-mp/.idea/misc.xml
  15. 8
      out/production/uav-edu-mp/.idea/modules.xml
  16. 11
      out/production/uav-edu-mp/.idea/uav-edu-mp.iml
  17. 6
      out/production/uav-edu-mp/.idea/vcs.xml
  18. BIN
      out/production/uav-edu-mp/A.class
  19. BIN
      out/production/uav-edu-mp/B.class
  20. BIN
      out/production/uav-edu-mp/Test.class
  21. 10
      out/production/uav-edu-mp/babel.config.js
  22. 43
      out/production/uav-edu-mp/components.d.ts
  23. 8
      out/production/uav-edu-mp/config/dev.js
  24. 135
      out/production/uav-edu-mp/config/index.js
  25. 36
      out/production/uav-edu-mp/config/prod.js
  26. 28239
      out/production/uav-edu-mp/package-lock.json
  27. 91
      out/production/uav-edu-mp/package.json
  28. 32
      out/production/uav-edu-mp/project.config.json
  29. 10
      out/production/uav-edu-mp/project.private.config.json
  30. 13
      out/production/uav-edu-mp/project.tt.json
  31. 33
      out/production/uav-edu-mp/src/app.config.js
  32. 27
      out/production/uav-edu-mp/src/app.js
  33. 0
      out/production/uav-edu-mp/src/app.less
  34. BIN
      out/production/uav-edu-mp/src/assets/deviceIcon.png
  35. BIN
      out/production/uav-edu-mp/src/assets/droneDisImg.png
  36. BIN
      out/production/uav-edu-mp/src/assets/droneImg.png
  37. BIN
      out/production/uav-edu-mp/src/assets/errPoint.png
  38. BIN
      out/production/uav-edu-mp/src/assets/fail.png
  39. BIN
      out/production/uav-edu-mp/src/assets/flower.png
  40. BIN
      out/production/uav-edu-mp/src/assets/home-marker.png
  41. 3
      out/production/uav-edu-mp/src/assets/home-marker.svg
  42. 199
      out/production/uav-edu-mp/src/assets/iconfont.less
  43. BIN
      out/production/uav-edu-mp/src/assets/iconfont.ttf
  44. BIN
      out/production/uav-edu-mp/src/assets/iconfont.woff
  45. BIN
      out/production/uav-edu-mp/src/assets/iconfont.woff2
  46. BIN
      out/production/uav-edu-mp/src/assets/list.png
  47. BIN
      out/production/uav-edu-mp/src/assets/lost-marker.png
  48. BIN
      out/production/uav-edu-mp/src/assets/point-marker.png
  49. 3
      out/production/uav-edu-mp/src/assets/point-marker.svg
  50. 3
      out/production/uav-edu-mp/src/assets/red-marker.svg
  51. BIN
      out/production/uav-edu-mp/src/assets/success.png
  52. BIN
      out/production/uav-edu-mp/src/assets/transparent-marker.png
  53. 3
      out/production/uav-edu-mp/src/assets/transparent-marker.svg
  54. 100
      out/production/uav-edu-mp/src/components/ExamQuestion.vue
  55. 209
      out/production/uav-edu-mp/src/components/ExamResult.vue
  56. 459
      out/production/uav-edu-mp/src/components/FlightDashboard.vue
  57. 206
      out/production/uav-edu-mp/src/components/RealTimeData.vue
  58. 271
      out/production/uav-edu-mp/src/components/RoutePointInfo.vue
  59. 56
      out/production/uav-edu-mp/src/components/TabBar.vue
  60. 131
      out/production/uav-edu-mp/src/components/TrackPlayback.vue
  61. 97
      out/production/uav-edu-mp/src/config/errorMap.js
  62. 18
      out/production/uav-edu-mp/src/config/fcSystemStatus.js
  63. 24
      out/production/uav-edu-mp/src/config/flyModeMap.js
  64. 41
      out/production/uav-edu-mp/src/config/gpsFixTypeMap.js
  65. 11
      out/production/uav-edu-mp/src/config/stageMap.js
  66. 187
      out/production/uav-edu-mp/src/config/tipTextMap.js
  67. 80
      out/production/uav-edu-mp/src/config/urls.js
  68. 165
      out/production/uav-edu-mp/src/core/FormData.js
  69. 108
      out/production/uav-edu-mp/src/core/SimpleAudioManager.js
  70. 92
      out/production/uav-edu-mp/src/core/helper/markHelper.js
  71. 281
      out/production/uav-edu-mp/src/core/helper/pinHelper.js
  72. 273
      out/production/uav-edu-mp/src/core/helper/sceneHelper.js
  73. 345
      out/production/uav-edu-mp/src/core/mimeMap.js
  74. 443
      out/production/uav-edu-mp/src/core/useDeviceCruise.js
  75. 466
      out/production/uav-edu-mp/src/core/useDeviceCruise2.js
  76. 49
      out/production/uav-edu-mp/src/core/useDrone.js
  77. 61
      out/production/uav-edu-mp/src/core/useErrPoint.js
  78. 138
      out/production/uav-edu-mp/src/core/useField.js
  79. 39
      out/production/uav-edu-mp/src/core/useLostPoint.js
  80. 164
      out/production/uav-edu-mp/src/core/useScene.js
  81. 34
      out/production/uav-edu-mp/src/core/useSpin.js
  82. 37
      out/production/uav-edu-mp/src/core/useTrack.js
  83. 58
      out/production/uav-edu-mp/src/core/useTripField.js
  84. 17
      out/production/uav-edu-mp/src/index.html
  85. 9
      out/production/uav-edu-mp/src/pages/airfield/index.config.js
  86. 454
      out/production/uav-edu-mp/src/pages/airfield/index.vue
  87. 101
      out/production/uav-edu-mp/src/pages/airfieldMap/BottomSide.vue
  88. 222
      out/production/uav-edu-mp/src/pages/airfieldMap/LeftSide.vue
  89. 124
      out/production/uav-edu-mp/src/pages/airfieldMap/ResultModal.vue
  90. 342
      out/production/uav-edu-mp/src/pages/airfieldMap/RightSide.vue
  91. 118
      out/production/uav-edu-mp/src/pages/airfieldMap/geo.js
  92. 5
      out/production/uav-edu-mp/src/pages/airfieldMap/index.config.js
  93. 452
      out/production/uav-edu-mp/src/pages/airfieldMap/index.vue
  94. 224
      out/production/uav-edu-mp/src/pages/airfieldMap/useConnector.js
  95. 331
      out/production/uav-edu-mp/src/pages/airfieldMap/useDroneMarker.js
  96. 6
      out/production/uav-edu-mp/src/pages/evaluation/index.config.js
  97. 628
      out/production/uav-edu-mp/src/pages/evaluation/index.vue
  98. 6
      out/production/uav-edu-mp/src/pages/flight/index.config.js
  99. 244
      out/production/uav-edu-mp/src/pages/flight/index.vue
  100. 113
      out/production/uav-edu-mp/src/pages/flightMap/BottomSide.vue

4
.env.development

@ -1,6 +1,6 @@
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config
# TARO_APP_ID="开发环境下的小程序 AppID"
TARO_APP_API="http://uavedu.jiagutech.com/api"
TARO_APP_WS_API="ws://uavedu.jiagutech.com/api"
TARO_APP_API="https://uavedu.jiagutech.com/api"
TARO_APP_WS_API="wss://uavedu.jiagutech.com/api"

4
.env.production

@ -1,3 +1,3 @@
# TARO_APP_ID="生产环境下的小程序 AppID"
TARO_APP_API="http://uavedu.jiagutech.com/api"
TARO_APP_WS_API="ws://uavedu.jiagutech.com/api"
TARO_APP_API="https://uavedu.jiagutech.com/api"
TARO_APP_WS_API="wss://uavedu.jiagutech.com/api"

2
.idea/misc.xml

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

4
.idea/uav-edu-mp.iml

@ -2,7 +2,9 @@
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

5
components.d.ts

@ -17,14 +17,18 @@ declare module 'vue' {
NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
NutInfiniteloading: typeof import('@nutui/nutui-taro')['Infiniteloading']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutInputNumber: typeof import('@nutui/nutui-taro')['InputNumber']
NutNotify: typeof import('@nutui/nutui-taro')['Notify']
NutOverlay: typeof import('@nutui/nutui-taro')['Overlay']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRadio: typeof import('@nutui/nutui-taro')['Radio']
NutRadioGroup: typeof import('@nutui/nutui-taro')['RadioGroup']
NutRange: typeof import('@nutui/nutui-taro')['Range']
NutSwitch: typeof import('@nutui/nutui-taro')['Switch']
NutTabbar: typeof import('@nutui/nutui-taro')['Tabbar']
NutTabbarItem: typeof import('@nutui/nutui-taro')['TabbarItem']
NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
@ -32,6 +36,7 @@ declare module 'vue' {
NutTag: typeof import('@nutui/nutui-taro')['Tag']
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
RealTimeData: typeof import('./src/components/RealTimeData.vue')['default']
RoutePointInfo: typeof import('./src/components/RoutePointInfo.vue')['default']
TabBar: typeof import('./src/components/TabBar.vue')['default']

14
out/production/uav-edu-mp/.editorconfig

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
max_line_length = 180
[*.md]
trim_trailing_whitespace = false

6
out/production/uav-edu-mp/.env.development

@ -0,0 +1,6 @@
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config
# TARO_APP_ID="开发环境下的小程序 AppID"
TARO_APP_API="https://uavedu.jiagutech.com/api"
TARO_APP_WS_API="wss://uavedu.jiagutech.com/api"

3
out/production/uav-edu-mp/.env.production

@ -0,0 +1,3 @@
# TARO_APP_ID="生产环境下的小程序 AppID"
TARO_APP_API="https://uavedu.jiagutech.com/api"
TARO_APP_WS_API="wss://uavedu.jiagutech.com/api"

1
out/production/uav-edu-mp/.env.test

@ -0,0 +1 @@
# TARO_APP_ID="测试环境下的小程序 AppID"

5
out/production/uav-edu-mp/.eslintrc

@ -0,0 +1,5 @@
// ESLint 检查 .vue 文件需要单独配置编辑器:
// https://eslint.vuejs.org/user-guide/#editor-integrations
{
"extends": ["taro/vue3"]
}

8
out/production/uav-edu-mp/.gitignore

@ -0,0 +1,8 @@
dist/
deploy_versions/
.temp/
.rn_temp/
node_modules/
.DS_Store
.swc
*.local

8
out/production/uav-edu-mp/.idea/.gitignore

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
out/production/uav-edu-mp/.idea/inspectionProfiles/Project_Default.xml

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>

6
out/production/uav-edu-mp/.idea/misc.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
out/production/uav-edu-mp/.idea/modules.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/uav-edu-mp.iml" filepath="$PROJECT_DIR$/.idea/uav-edu-mp.iml" />
</modules>
</component>
</project>

11
out/production/uav-edu-mp/.idea/uav-edu-mp.iml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
out/production/uav-edu-mp/.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

BIN
out/production/uav-edu-mp/A.class

Binary file not shown.

BIN
out/production/uav-edu-mp/B.class

Binary file not shown.

BIN
out/production/uav-edu-mp/Test.class

Binary file not shown.

10
out/production/uav-edu-mp/babel.config.js

@ -0,0 +1,10 @@
// babel-preset-taro 更多选项和默认值:
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
module.exports = {
presets: [
['taro', {
framework: 'vue3',
ts: false
}]
]
}

43
out/production/uav-edu-mp/components.d.ts

@ -0,0 +1,43 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ExamQuestion: typeof import('./src/components/ExamQuestion.vue')['default']
ExamResult: typeof import('./src/components/ExamResult.vue')['default']
FlightDashboard: typeof import('./src/components/FlightDashboard.vue')['default']
NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutCell: typeof import('@nutui/nutui-taro')['Cell']
NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
NutInfiniteloading: typeof import('@nutui/nutui-taro')['Infiniteloading']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutNotify: typeof import('@nutui/nutui-taro')['Notify']
NutOverlay: typeof import('@nutui/nutui-taro')['Overlay']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRadio: typeof import('@nutui/nutui-taro')['Radio']
NutRadioGroup: typeof import('@nutui/nutui-taro')['RadioGroup']
NutRange: typeof import('@nutui/nutui-taro')['Range']
NutTabbar: typeof import('@nutui/nutui-taro')['Tabbar']
NutTabbarItem: typeof import('@nutui/nutui-taro')['TabbarItem']
NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
NutTag: typeof import('@nutui/nutui-taro')['Tag']
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
RealTimeData: typeof import('./src/components/RealTimeData.vue')['default']
RoutePointInfo: typeof import('./src/components/RoutePointInfo.vue')['default']
TabBar: typeof import('./src/components/TabBar.vue')['default']
TrackPlayback: typeof import('./src/components/TrackPlayback.vue')['default']
}
}

8
out/production/uav-edu-mp/config/dev.js

@ -0,0 +1,8 @@
module.exports = {
env: {
NODE_ENV: '"development"'
},
defineConstants: {},
mini: {},
h5: {}
}

135
out/production/uav-edu-mp/config/index.js

@ -0,0 +1,135 @@
import Components from 'unplugin-vue-components/webpack';
import NutUIResolver from '@nutui/auto-import-resolver';
const config = {
projectName: 'canola-tool-mp',
date: '2025-3-14',
designWidth(input) {
if (input?.file?.replace(/\\+/g, '/').indexOf('@nutui') > -1) {
return 375
}
return 750
},
deviceRatio: {
640: 2.34 / 2,
750: 1,
828: 1.81 / 2,
375: 2 / 1
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: ['@tarojs/plugin-html'],
defineConstants: {},
copy: {
patterns: [],
options: {}
},
framework: 'vue3',
compiler: {
type: 'webpack5',
prebundle: { enable: false }
},
sass: {
data: `@import "@nutui/nutui-taro/dist/styles/variables.scss";`
},
mini: {
enableExtract: true,
miniCssExtractPluginOption: {
ignoreOrder: true,
},
webpackChain(chain) {
chain.plugin('unplugin-vue-components').use(Components({
resolvers: [
NutUIResolver({
importStyle: 'sass',
taro: true
})
]
})).merge({
module: {
rule: {
mjsScript: {
test: /\.mjs$/,
include: [/pinia/],
use: {
babelLoader: {
loader: require.resolve('babel-loader')
}
}
}
}
}
})
},
postcss: {
pxtransform: {
enable: true,
config: {
// selectorBlackList: ['nut-']
}
},
url: {
enable: true,
config: {
limit: 1, // 设定转换尺寸上限
}
},
cssModules: {
enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
},
h5: {
webpackChain(chain) {
chain.plugin('unplugin-vue-components').use(Components({
resolvers: [
NutUIResolver({
importStyle: 'sass',
taro: true
})
]
})).merge({
module: {
rule: {
mjsScript: {
test: /\.mjs$/,
include: [/pinia/],
use: {
babelLoader: {
loader: require.resolve('babel-loader')
}
}
}
}
}
})
},
publicPath: '/',
staticDirectory: 'static',
esnextModules: ['nutui-taro', 'icons-vue-taro'],
postcss: {
autoprefixer: {
enable: true,
config: {}
},
cssModules: {
enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
}
}
module.exports = function (merge) {
if (process.env.NODE_ENV === 'development') {
return merge({}, config, require('./dev'))
}
return merge({}, config, require('./prod'))
}

36
out/production/uav-edu-mp/config/prod.js

@ -0,0 +1,36 @@
module.exports = {
env: {
NODE_ENV: '"production"'
},
defineConstants: {},
mini: {},
h5: {
/**
* WebpackChain 插件配置
* @docs https://github.com/neutrinojs/webpack-chain
*/
// webpackChain (chain) {
// /**
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
// */
// chain.plugin('analyzer')
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
// /**
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
// */
// const path = require('path')
// const Prerender = require('prerender-spa-plugin')
// const staticDir = path.join(__dirname, '..', 'dist')
// chain
// .plugin('prerender')
// .use(new Prerender({
// staticDir,
// routes: [ '/pages/index/index' ],
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
// }))
// }
}
}

28239
out/production/uav-edu-mp/package-lock.json

File diff suppressed because it is too large

91
out/production/uav-edu-mp/package.json

@ -0,0 +1,91 @@
{
"name": "uav-edu-mp",
"version": "1.0.0",
"private": true,
"description": "",
"templateInfo": {
"name": "vue3-NutUI4",
"typescript": false,
"css": "Less",
"framework": "Vue3"
},
"scripts": {
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"build:qq": "taro build --type qq",
"build:jd": "taro build --type jd",
"build:quickapp": "taro build --type quickapp",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch"
},
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"author": "",
"dependencies": {
"@babel/runtime": "^7.7.7",
"@leafer-in/view": "^1.6.2",
"@leafer-in/viewport": "^1.6.3",
"@leafer-ui/miniapp": "^1.6.2",
"@leafer/miniapp": "^1.6.3",
"@nutui/icons-vue-taro": "^0.0.9",
"@nutui/nutui-taro": "4.3.13",
"@tarojs/components": "3.6.35",
"@tarojs/helper": "3.6.35",
"@tarojs/plugin-framework-vue3": "3.6.35",
"@tarojs/plugin-html": "3.6.35",
"@tarojs/plugin-platform-alipay": "3.6.35",
"@tarojs/plugin-platform-h5": "3.6.35",
"@tarojs/plugin-platform-jd": "3.6.35",
"@tarojs/plugin-platform-qq": "3.6.35",
"@tarojs/plugin-platform-swan": "3.6.35",
"@tarojs/plugin-platform-tt": "3.6.35",
"@tarojs/plugin-platform-weapp": "3.6.35",
"@tarojs/runtime": "3.6.35",
"@tarojs/shared": "3.6.35",
"@tarojs/taro": "3.6.35",
"@turf/turf": "^7.2.0",
"dayjs": "^1.11.13",
"gcoord": "^1.0.7",
"mathjs": "^14.4.0",
"pinia": "^2.2.6",
"popmotion": "^11.0.5",
"vue": "^3.2.40"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@nutui/auto-import-resolver": "^1.0.0",
"@tarojs/cli": "3.6.35",
"@tarojs/taro-loader": "3.6.35",
"@tarojs/webpack5-runner": "3.6.35",
"@types/node": "^18.15.11",
"@types/webpack-env": "^1.13.6",
"@vue/babel-plugin-jsx": "^1.0.6",
"@vue/compiler-sfc": "^3.2.40",
"babel-preset-taro": "3.6.35",
"css-loader": "3.4.2",
"eslint": "^8.12.0",
"eslint-config-taro": "3.6.35",
"eslint-plugin-vue": "^8.0.0",
"style-loader": "1.3.0",
"stylelint": "9.3.0",
"ts-node": "^10.9.1",
"typescript": "^4.1.0",
"unplugin-vue-components": "^0.26.0",
"vue-loader": "^17.0.0",
"webpack": "^5.78.0"
}
}

32
out/production/uav-edu-mp/project.config.json

@ -0,0 +1,32 @@
{
"miniprogramRoot": "dist/",
"projectname": "uav-edu-mp",
"description": "",
"appid": "wx0e317acde4e29bfa",
"setting": {
"urlCheck": true,
"es6": true,
"enhance": true,
"compileHotReLoad": false,
"postcss": false,
"minified": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"packNpmRelationList": []
},
"compileType": "miniprogram",
"libVersion": "3.6.5",
"srcMiniprogramRoot": "dist/",
"packOptions": {
"ignore": [],
"include": []
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
}
}

10
out/production/uav-edu-mp/project.private.config.json

@ -0,0 +1,10 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "uav-edu-mp",
"setting": {
"compileHotReLoad": true,
"bigPackageSizeSupport": true,
"urlCheck": false
},
"libVersion": "3.8.4"
}

13
out/production/uav-edu-mp/project.tt.json

@ -0,0 +1,13 @@
{
"miniprogramRoot": "./",
"projectname": "uav-edu-mp",
"description": "",
"appid": "touristappid",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram"
}

33
out/production/uav-edu-mp/src/app.config.js

@ -0,0 +1,33 @@
export default defineAppConfig({
pages: [
'pages/login/index',
'pages/studentMap2/index',
'pages/home/index',
'pages/studentMap/index',
'pages/airfieldMap/index',
'pages/supervisionMap/index',
'pages/airfield/index',
// 'pages/flightMap/index',
'pages/flightMap2/index',
'pages/supervision/index',
'pages/returnTripMap/index',
'pages/routePlanMap/index',
'pages/flight/index',
'pages/routePlan/index',
'pages/returnTrip/index',
'pages/evaluation/index',
'pages/own/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
},
// permission: {
// "scope.device": {
// "desc": "需要获取设备信息以提供更好的服务"
// }
// }
// requiredBackgroundModes: ["audio"]
})

27
out/production/uav-edu-mp/src/app.js

@ -0,0 +1,27 @@
import { createApp } from 'vue';
import store from './stores';
// import './app.less';
import './assets/iconfont.less'
// import './common.less';
import { IconFont } from '@nutui/icons-vue-taro';
import Taro from "@tarojs/taro";
const App = createApp({
onShow() {
// 保持屏幕常亮
Taro.setKeepScreenOn({
keepScreenOn: true
});
},
// 入口组件不需要实现 render 方法,即使实现了也会被 taro 所覆盖
})
App.config.warnHandler = () => {};
App.use(store);
App.use(IconFont);
export default App;

0
out/production/uav-edu-mp/src/app.less

BIN
out/production/uav-edu-mp/src/assets/deviceIcon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
out/production/uav-edu-mp/src/assets/droneDisImg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
out/production/uav-edu-mp/src/assets/droneImg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
out/production/uav-edu-mp/src/assets/errPoint.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

BIN
out/production/uav-edu-mp/src/assets/fail.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
out/production/uav-edu-mp/src/assets/flower.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
out/production/uav-edu-mp/src/assets/home-marker.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

3
out/production/uav-edu-mp/src/assets/home-marker.svg

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="19" fill="#4CAF50" stroke="#FFFFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 174 B

199
out/production/uav-edu-mp/src/assets/iconfont.less

@ -0,0 +1,199 @@
@font-face {
font-family: "iconfont"; /* Project id 4857170 */
src: url('./iconfont.woff2?t=1746257417746') format('woff2'),
url('./iconfont.woff?t=1746257417746') format('woff'),
url('./iconfont.ttf?t=1746257417746') format('truetype')
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-angle-speed:before {
content: "\e8c2";
}
.icon-rotate360:before {
content: "\e6f3";
}
.icon-lock:before {
content: "\e861";
}
.icon-unlock:before {
content: "\e862";
}
.icon-enter-center:before {
content: "\e642";
}
.icon-heading:before {
content: "\e687";
}
.icon-tangent-speed:before {
content: "\e6e6";
}
.icon-stability:before {
content: "\e611";
}
.icon-throttle:before {
content: "\e743";
}
.icon-arrow-north:before {
content: "\e905";
}
.icon-playback:before {
content: "\e60a";
}
.icon-flag:before {
content: "\e66b";
}
.icon-air-route:before {
content: "\e809";
}
.icon-char8:before {
content: "\e6b1";
}
.icon-tangent-angle:before {
content: "\e6b0";
}
.icon-stopwatch:before {
content: "\e62b";
}
.icon-offset-h:before {
content: "\e753";
}
.icon-offset-v:before {
content: "\ee1a";
}
.icon-bg-arrow:before {
content: "\e6af";
}
.icon-arrow:before {
content: "\e6ad";
}
.icon-block:before {
content: "\e6ae";
}
.icon-stage:before {
content: "\e6fe";
}
.icon-lib:before {
content: "\e6bb";
}
.icon-add-point:before {
content: "\e7e4";
}
.icon-topic:before {
content: "\e60f";
}
.icon-mistake:before {
content: "\e600";
}
.icon-compass:before {
content: "\ee19";
}
.icon-speaker:before {
content: "\e652";
}
.icon-home-point:before {
content: "\e648";
}
.icon-waypoint:before {
content: "\e6ac";
}
.icon-measure:before {
content: "\e609";
}
.icon-location:before {
content: "\e608";
}
.icon-positioning:before {
content: "\e647";
}
.icon-zoom-in:before {
content: "\e6c4";
}
.icon-zoom-out:before {
content: "\e6c9";
}
.icon-uav4:before {
content: "\e736";
}
.icon-battery:before {
content: "\e6a9";
}
.icon-satellite:before {
content: "\e6aa";
}
.icon-fn:before {
content: "\e781";
}
.icon-setting:before {
content: "\e699";
}
.icon-scan:before {
content: "\e607";
}
.icon-help:before {
content: "\e63b";
}
.icon-book:before {
content: "\e638";
}
.icon-time:before {
content: "\e61d";
}
.icon-panel:before {
content: "\e60d";
}
.icon-visual:before {
content: "\e603";
}

BIN
out/production/uav-edu-mp/src/assets/iconfont.ttf

Binary file not shown.

BIN
out/production/uav-edu-mp/src/assets/iconfont.woff

Binary file not shown.

BIN
out/production/uav-edu-mp/src/assets/iconfont.woff2

Binary file not shown.

BIN
out/production/uav-edu-mp/src/assets/list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

BIN
out/production/uav-edu-mp/src/assets/lost-marker.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

BIN
out/production/uav-edu-mp/src/assets/point-marker.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

3
out/production/uav-edu-mp/src/assets/point-marker.svg

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="19" fill="#1989fa" stroke="#FFFFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

3
out/production/uav-edu-mp/src/assets/red-marker.svg

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="19" fill="#FF0000" stroke="white" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 185 B

BIN
out/production/uav-edu-mp/src/assets/success.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
out/production/uav-edu-mp/src/assets/transparent-marker.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

3
out/production/uav-edu-mp/src/assets/transparent-marker.svg

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="19" fill="transparent" />
</svg>

After

Width:  |  Height:  |  Size: 146 B

100
out/production/uav-edu-mp/src/components/ExamQuestion.vue

@ -0,0 +1,100 @@
<script setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
isVisible: {
type: Boolean,
default: false
},
questionData: {
type: Object,
default: () => ({
imageUrl: '',
content: ''
})
}
});
const isExpanded = ref(false);
//
watch(() => props.isVisible, (newVal) => {
if (newVal) {
setTimeout(() => {
isExpanded.value = true;
}, 50);
} else {
isExpanded.value = false;
}
});
</script>
<template>
<div :class="[s.root, { 'is-expanded': isExpanded }]" v-if="isVisible">
<div class="title">
<div style="display: flex; align-items: center; gap: 4px;">
<t-icon name="file" />
<div>考题</div>
</div>
</div>
<div class="content">
<div class="question-image" v-if="questionData.imageUrl">
<img :src="questionData.imageUrl" alt="考题图片" />
</div>
<div class="requirements" v-html="questionData.content"></div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 80%;
max-width: 400px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
color: #fff;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
z-index: 100;
display: flex;
flex-direction: column;
&.is-expanded {
transform: translateX(0);
}
:global {
.title {
padding: 16px;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.content {
flex: 1;
padding: 16px;
overflow-y: auto;
.question-image {
margin-bottom: 16px;
img {
width: 100%;
border-radius: 8px;
}
}
.requirements {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
}
}
}
}
</style>

209
out/production/uav-edu-mp/src/components/ExamResult.vue

@ -0,0 +1,209 @@
<script setup>
// import { ref } from 'vue';
// import { Left, Right } from '@nutui/icons-vue-taro';
import {formatTime} from "../utils/helpers";
const props = defineProps({
info: {
type: Object,
default: () => ({})
},
});
</script>
<template>
<div :class="s.root" :catch-move="true">
<div class="title">
<div style="display: flex; align-items: center; gap: 4px;">
<div>考核结果</div>
</div>
</div>
<div class="content">
<div class="basic-info">
<div class="row"><span class="label">学员</span><span class="att">{{ info.studentName }}</span></div>
<div class="row"><span class="label">教员</span><span class="att">{{ info.teacherName }}</span></div>
<div class="row"><span class="label">开始时间</span><span class="att">{{ formatTime(info.beginTime) }}</span></div>
<div class="row"><span class="label">结束时间</span><span class="att">{{ formatTime(info.finishTime) }}</span></div>
</div>
<div class="result-info">
<div class="status" :class="{ 'passed': info.isPass, 'failed': !info.isPass }">
{{ info.isPass ? '通过' : '未通过' }}
</div>
<div v-if="!info.isPass && (info?.errors || []).length > 0" class="error-points">
<div class="error-title">错误信息</div>
<div v-for="(point, index) in info?.errors" :key="index" class="error-item">
<div class="error-detail">
<div class="error-row">
<span class="label">当前值</span>
<span class="value">{{ point.current }}</span>
</div>
<div class="error-row">
<span class="label">合格值</span>
<span class="value">{{ point.qualified }}</span>
</div>
</div>
<div class="error-message">{{ point.message }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
right: 5px;
top: 5px;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border-radius: 8px;
color: white;
font-size: 10px;
width: 22vw;
min-width: 140px;
max-height: 80vh;
overflow: auto;
z-index: 2;
//transition: transform 0.3s ease;
//&.hidden {
// transform: translateX(calc(100% + 5px));
//}
:global {
.toggle-btn {
width: 16px;
height: 16px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 20px;
transform: translateX(-100%);
&:hover {
background-color: rgba(0, 0, 0, 0.8);
}
&.is-active {
background-color: rgba(0, 0, 0, 0.9);
}
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px 4px 8px;
font-weight: 500;
}
.content {
max-height: 60vh;
overflow: auto;
padding: 6px;
.basic-info {
.row {
display: flex;
align-items: center;
.label {
text-align: right;
width: 50px;
}
.att {
flex: 1;
}
}
}
.result-info {
.status {
font-size: 12px;
padding: 6px 8px;
border-radius: 4px;
font-weight: bold;
text-align: center;
// margin-bottom: 8px;
&.passed {
background-color: rgba(0, 255, 0, 0.2);
color: #52c41a;
}
&.failed {
background-color: rgba(255, 0, 0, 0.2);
color: #ff4d4f;
}
}
.error-detail {
white-space: nowrap;
}
.error-message {
font-size: 10px;
color: rgba(255, 255, 255, 0.85);
line-height: 1.4;
text-align: center;
word-break: break-all;
}
.error-points {
margin-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 8px;
.error-title {
font-weight: 500;
margin-bottom: 6px;
color: #ff6b6b;
}
.error-item {
background-color: rgba(255, 107, 107, 0.1);
padding: 6px;
border-radius: 4px;
margin-bottom: 6px;
.error-detail {
margin-bottom: 6px;
.error-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
.label {
color: #ff9999;
font-size: 10px;
}
.value {
font-size: 10px;
color: #fff;
}
}
}
.error-message {
color: #ff9999;
font-size: 10px;
line-height: 1.4;
}
}
}
}
}
}
}
</style>

459
out/production/uav-edu-mp/src/components/FlightDashboard.vue

@ -0,0 +1,459 @@
<script setup>
import { onMounted, ref, watch, onUnmounted } from 'vue';
import Taro from '@tarojs/taro';
const props = defineProps({
pitch: {
type: Number,
default: 0
},
roll: {
type: Number,
default: 0
},
yaw: {
type: Number,
default: 0
},
homeAngle: {
type: Number,
default: 0
},
showHome: {
type: Boolean,
default: true
},
size: {
type: Number,
default: 150
},
homeDist: {
type: Number,
default: 0,
},
hvel: {
type: Number,
default: 0,
},
height: {
type: Number,
default: 0,
}
});
const canvasContext = ref(null);
//
const drawDashboard = () => {
const ctx = canvasContext.value;
if (!ctx) return;
//
ctx.restore();
ctx.save();
const width = props.size;
const height = props.size;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - (props.showHome? 8 : 4);
//
ctx.clearRect(0, 0, width, height);
//
ctx.beginPath();
ctx.arc(centerX, centerY, radius + (props.showHome ? 4 : 2), 0, 2 * Math.PI);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = props.showHome? 8 : 4;
ctx.stroke();
//
ctx.save();
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.clip();
//
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((props.roll * Math.PI) / 180);
const horizonY = props.pitch * 1.0;
//
ctx.beginPath();
ctx.rect(-width/2, -height/2, width, height/2 + horizonY);
ctx.fillStyle = '#1E90FF';
ctx.fill();
//
ctx.beginPath();
ctx.rect(-width/2, horizonY, width, height/2 - horizonY);
ctx.fillStyle = '#FFA500';
ctx.fill();
ctx.restore(); //
// 线
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((-props.yaw * Math.PI) / 180);
// 线
for (let i = 0; i < 36; i++) {
const angle = (i * 10 * Math.PI) / 180;
const isMainScale = i % 9 === 0; // 90
const isNorthDirection = i === 0;
const scaleLength = isMainScale ? 12 : 6;
// 线
ctx.beginPath();
ctx.moveTo(
Math.cos(angle - Math.PI/2) * (radius - scaleLength),
Math.sin(angle - Math.PI/2) * (radius - scaleLength)
);
ctx.lineTo(
Math.cos(angle - Math.PI/2) * radius,
Math.sin(angle - Math.PI/2) * radius
);
// 线
if (isNorthDirection) {
ctx.strokeStyle = '#FF0000'; // 线
ctx.lineWidth = 2;
} else {
ctx.strokeStyle = isMainScale ? 'rgba(255, 255, 255, 0.95)' : 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = isMainScale ? 1.5 : 1;
}
ctx.shadowColor = 'rgba(255, 255, 255, 0.3)';
ctx.shadowBlur = 4;
ctx.stroke();
//
if (isMainScale) {
let direction = '';
if (i === 0) direction = 'N';
else if (i === 9) direction = 'E';
else if (i === 18) direction = 'S';
else if (i === 27) direction = 'W';
const textX = Math.cos(angle - Math.PI/2) * (radius - 20);
const textY = Math.sin(angle - Math.PI/2) * (radius - 20);
ctx.fillStyle = isNorthDirection ? '#FF0000' : '#000000';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(direction, textX, textY);
}
}
ctx.restore();
// 线
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate((props.roll * Math.PI) / 180);
// -180°180°5°
for (let angle = -180; angle <= 180; angle += 5) {
const y = horizonY - angle;
// const isMainScale = angle % 30 === 0;
const isSecondaryScale = angle % 10 === 0;
const isZeroScale = angle === 0;
// 线
ctx.beginPath();
if (isZeroScale) {
ctx.moveTo(10, y);
ctx.lineTo(-10, y);
ctx.strokeStyle = 'rgba(85,85,85,0.4)';
ctx.lineWidth = 3;
// } else if (isMainScale) {
// ctx.moveTo(15, y);
// ctx.lineTo(-15, y);
// ctx.strokeStyle = 'rgba(85,85,85,0.4)';
// ctx.lineWidth = 2;
} else if (isSecondaryScale) {
ctx.moveTo(10, y);
ctx.lineTo(-10, y);
ctx.strokeStyle = 'rgba(85,85,85,0.4)';
ctx.lineWidth = 1.5;
} else {
// ctx.moveTo(15, y);
// ctx.lineTo(-15, y);
// ctx.strokeStyle = 'rgba(85,85,85,0.8)';
// ctx.lineWidth = 1;
ctx.moveTo(6, y);
ctx.lineTo(-6, y);
ctx.strokeStyle = 'rgba(85,85,85,0.4)';
ctx.lineWidth = 1;
}
ctx.stroke();
// // 线
// ctx.beginPath();
// if (isZeroScale) {
// ctx.moveTo(15, y);
// ctx.lineTo(25, y);
// } else if (isMainScale) {
// ctx.moveTo(15, y);
// ctx.lineTo(25, y);
// } else if (isSecondaryScale) {
// ctx.moveTo(15, y);
// ctx.lineTo(20, y);
// } else {
// ctx.moveTo(15, y);
// ctx.lineTo(18, y);
// }
// ctx.stroke();
// 30
if (angle % 30 === 0 && angle !== 0) {
ctx.fillStyle = 'rgba(85,85,85,0.4)';
ctx.font = '10px Arial';
//
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(angle.toString(), -11, y);
//
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(angle.toString(), 11, y);
}
// 0
if (isZeroScale) {
ctx.fillStyle = 'rgba(85,85,85,0.4)';
ctx.font = 'bold 10px Arial';
// 0
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText('0°', -11, y);
// 0
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('0°', 11, y);
}
}
ctx.restore();
// 姿
ctx.translate(centerX, centerY);
// "V"
ctx.beginPath();
//
ctx.moveTo(-30, 0);
ctx.lineTo(-7, 0);
// V
ctx.lineTo(0, 5);
ctx.lineTo(7, 0);
//
ctx.lineTo(30, 0);
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; //
ctx.lineWidth = 2; // 线
ctx.stroke();
//
ctx.beginPath();
ctx.arc(0, 0, 1.5, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';
ctx.fill();
//
ctx.beginPath();
ctx.moveTo(-6, -60);
ctx.lineTo(6, -60);
ctx.lineTo(0, -68);
ctx.closePath();
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)'; // fillStyle
ctx.fill();
ctx.restore(); //
// Home
if (props.showHome) {
// ctx.save();
ctx.translate(centerX, centerY);
// Home
// console.log('aaa', props.homeAngle);
const homeAngle = (props.homeAngle * Math.PI) / 180;
// const homeAngle = props.homeAngle;
const homeDistance = radius + 4; // Home
// const homeX = Math.cos(homeAngle - Math.PI / 2) * homeDistance;
// const homeY = Math.sin(homeAngle - Math.PI / 2) * homeDistance;
const homeX = Math.cos(homeAngle - Math.PI / 2) * homeDistance;
const homeY = Math.sin(homeAngle - Math.PI / 2) * homeDistance;
// Home
ctx.beginPath();
ctx.arc(homeX, homeY, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#009700';
ctx.fill();
// Home
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 8px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; //
ctx.fillText('H', homeX, homeY);
ctx.restore();
}
};
onMounted(() => {
const query = Taro.createSelectorQuery();
query.select('#leafer2')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
//
const dpr = Taro.getSystemInfoSync().pixelRatio || 1;
// canvas
canvas.width = props.size * dpr;
canvas.height = props.size * dpr;
//
ctx.scale(dpr, dpr);
//
//
canvasContext.value = ctx;
//
drawDashboard();
});
//
watch(() => ({
pitch: props.pitch,
roll: props.roll,
yaw: props.yaw,
homeAngle: props.homeAngle,
showHome: props.showHome,
size: props.size //
}), () => {
drawDashboard();
}, { deep: true });
});
// watch(() => props, (newProps) => {
// drawDashboard();
// }, {deep: true});
onUnmounted(() => {
//
})
</script>
<template>
<div :class="s.root">
<canvas
id="leafer2"
type="2d"
:style="{ width: `${props.size}px`, height: `${props.size}px` }"
></canvas>
<div class="attitude-info">
<div class="attitude-item">
<span class="info-label">俯仰角</span>
<span class="info-value">{{ pitch }}°</span>
</div>
<div class="attitude-item">
<span class="info-label">横滚角</span>
<span class="info-value">{{ roll }}°</span>
</div>
<div class="attitude-item">
<span class="info-label">偏航角</span>
<span class="info-value">{{ (((yaw ?? 0) + 360) % 360).toFixed(2) }}°</span>
</div>
</div>
<div class="attitude-info">
<div class="attitude-item">
<span class="info-label">离H点</span>
<span class="info-value">{{ homeDist }}m</span>
</div>
<div class="attitude-item">
<span class="info-label">高度</span>
<span class="info-value">{{ height }}m</span>
</div>
<div class="attitude-item">
<span class="info-label">水平速度</span>
<span class="info-value">{{ hvel }}m/s</span>
</div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
// position: absolute;
// top: 0;
// bottom: 0;
// right: 0;
// left: 0;
z-index: 2;
//display: flex;
//align-items: flex-start;
//gap: 16px;
// border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
:global {
#leafer2 {
width: 150px;
height: 150px;
flex-shrink: 0;
}
.attitude-info {
//background-color: #7f8c8d;
display: flex;
gap: 6px;
padding: 2px;
// background: rgba(0, 0, 0, 0.6);
border-radius: 8px;
width: 100%;
justify-content: center;
.attitude-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 40px;
.info-label {
font-size: 10px;
color: rgba(255, 255, 255, 0.8);
}
.info-value {
font-size: 10px;
color: #ffffff;
font-family: monospace;
}
}
}
}
}
</style>

206
out/production/uav-edu-mp/src/components/RealTimeData.vue

@ -0,0 +1,206 @@
<script setup>
import { computed, ref } from 'vue';
import { Left, Right } from '@nutui/icons-vue-taro';
import { toFixed } from '../utils/helpers';
import { formatTime } from '../utils/helpers';
// import { useDeviceCruise } from '../core/useDeviceCruise';
// const { timelyData } = useDeviceCruise();
const isVisible = ref(true);
// const toggleVisibility = () => {
// isVisible.value = !isVisible.value;
// };
// const showTip = ref(true);
// const toggleBtnClass = computed(() => {
// return {
// 'toggle-btn': true,
// 'is-active': !isVisible.value
// };
// });
const info = computed(() => ({}));
</script>
<template>
<div :class="[s.root, { [s.hidden]: !isVisible }]">
<div class="nav">
<div :class="{ 'toggle-btn': true, 'is-active': !isVisible }" @click="() => isVisible = !isVisible">
<Left v-if="isVisible" style="font-size: 14px;" />
<Right style="font-size: 14px;" v-else />
</div>
</div>
<div class="text-row">
<span class="label">时间</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ formatTime(info.timestamp) }}</small>
<span class="unit"></span>
</div>
</span>
</div>
<div class="text-row">
<span class="label">经度</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.lng, 7) }}</small>
<span class="unit">°</span>
</div>
</span>
</div>
<div class="text-row">
<span class="label">纬度</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.lat, 7) }}</small>
<span class="unit">°</span>
</div>
</span>
</div>
<div class="text-row" v-if="(info.xspeed === 0) || info.xspeed">
<span class="label">水平速度</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.xspeed, 2) }}</small>
<span class="unit">/</span>
</div>
</span>
</div>
<div class="text-row" v-if="(info.velocity === 0) || info.velocity">
<span class="label">速度</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.velocity, 2) }}</small>
<span class="unit">千米/小时</span>
</div>
</span>
</div>
<div class="text-row" v-if="(info.breadth === 0) || info.breadth">
<span class="label">幅宽</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.breadth, 2) }}</small>
<span class="unit"></span>
</div>
</span>
</div>
<div class="text-row" v-if="(info.deep === 0) || info.deep">
<span class="label">耕深</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.deep, 2) }}</small>
<span class="unit">厘米</span>
</div>
</span>
</div>
<div class="text-row" v-if="(info.seeding === 0) || info.seeding">
<span class="label">播种速度</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ info.seeding }}</small>
<span class="unit">/s</span>
</div>
</span>
</div>
<div class="text-row">
<span class="label">航向角</span>
<span class="value">
<div class="value-unit">
<small class="value">{{ toFixed(info.yaw, 2) }}</small>
<span class="unit">°</span>
</div>
</span>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
left: 5px;
top: 5px;
background-color: rgba(0, 0, 0, 0.6);
padding: 8px;
border-radius: 8px;
color: white;
font-size: 11px;
width: fit-content;
min-width: 120px;
z-index: 2;
transition: transform 0.3s ease;
&.hidden {
transform: translateX(calc(-100% - 5px));
}
:global {
.nav {
position: absolute;
right: -20px;
top: 6px;
.toggle-btn {
width: 20px;
height: 20px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 0 4px 4px 0;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
}
&.is-active {
background-color: rgba(0, 0, 0, 0.9);
}
}
}
}
:global {
.text-row {
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 2px;
&:last-child {
margin-bottom: 0;
}
.label {
min-width: 40px;
color: rgba(255, 255, 255, 0.8);
font-size: 10px;
}
.value {
color: white;
}
.value-unit {
display: inline-flex;
align-items: baseline;
gap: 1px;
.value {
font-weight: 500;
}
.unit {
font-size: 0.85em;
opacity: 0.8;
}
small {
font-size: 0.85em;
}
}
}
}
}
</style>

271
out/production/uav-edu-mp/src/components/RoutePointInfo.vue

@ -0,0 +1,271 @@
<script setup>
import { computed, ref } from 'vue';
import { Left, Right } from '@nutui/icons-vue-taro';
import { toFixed } from '../utils/helpers';
import * as turf from "@turf/turf";
// const isVisible = ref(true);
// const pointInfoList = computed(() => [
// {
// index: 1,
// angle: 0,
// distance: 90,
// height: 50,
// stayTime: 0
// },
// {
// index: 2,
// angle: 135,
// distance: 70,
// height: 60,
// stayTime: 2
// },
// {
// index: 3,
// angle: 225,
// distance: 80,
// height: 45,
// stayTime: 3
// },
// {
// index: 4,
// angle: 315,
// distance: 100,
// height: 55,
// stayTime: 2
// }
// ]);
const props = defineProps({
pointInfoList: {
type: Array,
// required: true,
default: () => [],
},
homeLngLat: {
type: Array,
default: () => [],
}
});
const disArray = computed(() => {
if (!props.pointInfoList.length) return [];
const tmp2 = props.pointInfoList.map(item => [item.longitude, item.latitude]);
const tmp = [props.homeLngLat, ...tmp2];
const disA = [];
for (let i = 0; i < tmp.length - 1; i++) {
// const tmp = calculateDistance(points[i], points[i + 1]);
const distance = turf.distance(tmp[i], tmp[i + 1], { units: 'meters' }).toFixed(1);
disA.push(distance);
// const tmp = turf.midpoint(planPoint[i], planPoint[i + 1]);
// const { geometry: { coordinates = [] } = {} } = tmp;
// markers.value.push({
// id: 1e7 + i,
// ...midMarkerOption,
// longitude: coordinates[0],
// latitude: coordinates[1],
// label: {
// content: `${distance}m`,
// ...markerOptions,
// }
// });
}
return disA;
})
const angleArr = computed(() => {
if (!props.pointInfoList.length) return [];
const tmp2 = props.pointInfoList.map(item => [item.longitude, item.latitude]);
const tmp = [props.homeLngLat, ...tmp2];
const angleA = [];
for (let i = 0; i < tmp.length - 1; i++) {
// const tmp = calculateDistance(points[i], points[i + 1]);
const angle = turf.bearing(tmp[i], tmp[i + 1]);
const a = (((angle ?? 0) + 360) % 360).toFixed(1);
angleA.push(a);
// const tmp = turf.midpoint(planPoint[i], planPoint[i + 1]);
// const { geometry: { coordinates = [] } = {} } = tmp;
// markers.value.push({
// id: 1e7 + i,
// ...midMarkerOption,
// longitude: coordinates[0],
// latitude: coordinates[1],
// label: {
// content: `${distance}m`,
// ...markerOptions,
// }
// });
}
return angleA;
})
// const emit = defineEmits(['update:isVisible']);
// const toggleVisibility = () => {
// emit('update:isVisible', !props.isVisible);
// };
</script>
<template>
<div :class="s.root">
<div class="title">
<div style="display: flex; align-items: center; gap: 4px;">
<div>航线点信息</div>
</div>
</div>
<div class="content">
<div class="route-point-info">
<div class="point-card" v-for="(item, index) in pointInfoList" :key="index">
<div class="point-header">
<div class="point-index">{{ index + 1 }}</div>
</div>
<div class="point-details">
<div class="detail-row">
<span class="label">方位角</span>
<span class="value">{{ angleArr[index] }}°</span>
</div>
<div class="detail-row">
<span class="label">相对距离</span>
<span class="value">{{ disArray[index] }}</span>
</div>
<div class="detail-row">
<span class="label">高度</span>
<span class="value">{{ toFixed(item.height, 2) }}</span>
</div>
<div class="detail-row">
<span class="label">停留时长</span>
<span class="value">{{ item.duration }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
left: 5px;
top: 5px;
background-color: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(20px);
border-radius: 8px;
color: white;
font-size: 10px;
width: fit-content;
min-width: 17vw;
//max-height: 60vh;
z-index: 2;
//overflow: auto;
//transition: transform 0.3s ease;
//&.hidden {
// //transform: translateX(calc(-100% - 10px));
//}
:global {
.title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px 4px 8px;
}
.toggle-btn {
width: 20px;
height: 20px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.6);
// border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
// transition: all 0.3s ease;
position: absolute;
right: 0;
top: 20px;
transform: translateX(100%);
&:hover {
background-color: rgba(0, 0, 0, 0.8);
}
&.is-active {
background-color: rgba(0, 0, 0, 0.9);
}
}
.content {
padding: 6px;
overflow: auto;
.route-point-info {
max-height: 60vh;
overflow: auto;
.point-card {
background-color: rgba(56, 56, 56, 0.9);
border-radius: 8px;
padding: 8px;
display: flex;
align-items: flex-start;
gap: 3px;
.point-header {
display: flex;
align-items: center;
.point-index {
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid rgba(254, 254, 254, 0.4);
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
}
}
.point-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
width: 100%;
.detail-row {
display: flex;
align-items: center;
justify-content: space-between;
white-space: nowrap;
gap: 4px;
line-height: 1.2;
.label {
font-size: 9px;
color: rgba(255, 255, 255, 0.6);
min-width: 20px;
}
.value {
font-size: 9px;
color: rgba(255, 255, 255, 0.95);
text-align: right;
}
}
}
& + .point-card {
margin-top: 8px;
}
}
}
}
}
}
</style>

56
out/production/uav-edu-mp/src/components/TabBar.vue

@ -0,0 +1,56 @@
<script setup>
import { eventCenter, getCurrentInstance, reLaunch } from '@tarojs/taro'
import {onMounted, ref, h, computed} from "vue";
import { Home, Category, My, Eye, Horizontal } from '@nutui/icons-vue-taro';
import { useAuthStore } from "../stores";
import { storeToRefs } from 'pinia';
const { isStudent } = storeToRefs(useAuthStore());
const current = ref(0);
const tabList = computed(() => [
...(isStudent.value ? [
{ title: '首页', icon: h(Home), path: '/pages/home/index' },
// { title: '', icon: h(Eye), path: '/pages/supervision/index' },
// { title: '', icon: h(Category), path: '/pages/airfield/index' },
{ title: '我的', icon: h(My), path: '/pages/own/index' }
] : [
{ title: '监看', icon: h(Eye), path: '/pages/supervision/index' },
{ title: '记录', icon: h(Horizontal), path: '/pages/home/index' },
{ title: '场地', icon: h(Category), path: '/pages/airfield/index' },
{ title: '我的', icon: h(My), path: '/pages/own/index' }
]),
]);
const emit = defineEmits(['change']);
function handleClick(_, index) {
const item = tabList.value[index];
emit('change', item);
if (item.path) {
reLaunch({ url: item.path })
current.value = index;
}
}
onMounted(() => {
eventCenter.on(getCurrentInstance().router.onShow, () => {
const page = getCurrentInstance().router.path;
if (!page) return;
const index = tabList.value.findIndex(item => item.path === page);
if (index !== -1) current.value = index;
});
})
</script>
<template>
<nut-tabbar v-model="current" bottom safe-area-inset-bottom placeholder unactive-color="#7d7e80" active-color="#1989fa" @tab-switch="handleClick">
<nut-tabbar-item v-for="(item, index) in tabList" :key="index" :tab-title="item.title" :icon="item.icon">
</nut-tabbar-item>
</nut-tabbar>
</template>
<style lang="less" module="s">
</style>

131
out/production/uav-edu-mp/src/components/TrackPlayback.vue

@ -0,0 +1,131 @@
<script setup>
import { computed, ref } from 'vue';
// import deviceCruise from '../core/useDeviceCruise';
import { PlayStart, PlayStop } from '@nutui/icons-vue-taro';
const props = defineProps({
info: {
type: Object,
default: () => ({})
}
})
//
const ready = computed(() => props.info?.ready);
// const speedRate = computed(() => props.info?.speedRate);
const totalTime = computed(() => props.info?.totalTime);
const currentTime = computed({
get() {
return props.info?.elapsedMs;
},
set(val) {
// console.log('aaaaa', val);
// deviceCruise.handlePause();
props.info?.setCurrentTime(val);
},
});
const showPlayButton = computed(() => props.info?.isPaused);
//
function formatTime(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
//
const currentTimeDisplay = computed(() => formatTime(props.info?.elapsedMs));
const totalTimeDisplay = computed(() => formatTime(props.info?.totalTime));
// //
// function onChangeSpeedRate() {
// const newSpeedRate = speedRate.value >= 8 ? 1 : speedRate.value * 2;
// deviceCruise.setSpeedRate(newSpeedRate);
// }
function onPlay() {
props.info?.play();
}
function onPause() {
props.info?.pause();
}
</script>
<template>
<view :class="s.root" :catch-move="true">
<view class="btn-box">
<view class="btn" v-if="showPlayButton" @click="onPlay">
<PlayStart />
</view>
<view class="btn" v-else @click="onPause">
<PlayStop />
</view>
</view>
<view class="slider-container">
<nut-range
v-model="currentTime"
hidden-range
:hidden-tag="true"
:max="totalTime"
:min="0"
:step="1"
/>
<view class="time-display">
<text>{{ currentTimeDisplay }}</text>/
<text>{{ totalTimeDisplay }}</text>
</view>
</view>
</view>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
left: 150px;
right: 150px;
bottom: 40px;
z-index: 999;
//padding: 10rpx 15rpx 10rpx 15rpx;
//border-radius: 8rpx;
display: flex;
// flex-direction: row;
align-items: center;
//gap: 20rpx;
:global {
.btn-box {
color: white;
//background: rgba(0, 0, 0, 0.5);
margin-right: 8px;
}
.slider-container {
flex: 1;
display: flex;
align-items: center;
.nut-range-button {
width: 10px;
height: 10px;
}
.nut-range {
height: 4px;
}
}
.time-display {
//display: flex;
//justify-content: space-between;
font-size: 11px;
font-weight: bold;
color: #fff;
white-space: nowrap;
margin-left: 10px;
}
}
}
</style>

97
out/production/uav-edu-mp/src/config/errorMap.js

@ -0,0 +1,97 @@
/**
* 故障报警
* 参考1http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#22-%E7%B3%BB%E7%BB%9F%E7%8A%B6%E6%80%81-sys_status
* 参考2http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#16-%E9%94%99%E8%AF%AF%E7%8A%B6%E6%80%811-vkfly_sys_error1
*/
export const ERRORS_COUNT_1 = {
1: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604421642084386.mp3",
text: '地面站失联',
},
2: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604473069342913.mp3",
text: '电池电压低',
},
4: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604622658102213.mp3",
text: '电机平衡差',
},
8: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604672460876362.mp3",
text: '动力故障',
},
16: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604720009179855.mp3",
text: '飞控温度高',
},
32: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604764931252051.mp3",
text: '飞控无INS解算定位',
},
64: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604829931352407.mp3",
text: '超出电子围栏范围',
},
128: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604870398190001.mp3",
text: '备用链路失联'
},
256: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604912984080884.mp3",
text: '智能电池bms数据失联',
},
512: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604950827592367.mp3",
text: '发动机油量低'
},
1024: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746604990069674822.mp3",
text: '发动机数据断开'
},
2048: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605027377313620.mp3",
text: '氢气压低'
},
};
export const ERRORS_COUNT_2 = {};
export const ERRORS_COUNT_3 = {
1: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605075004362038.mp3",
text: 'mag1磁干扰'
},
2: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605119468335695.mp3",
text: 'mag2磁干扰'
},
4: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605149049385111.mp3",
text: 'imu1数据异常'
},
8: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605234388382865.mp3",
text: 'imu2数据异常'
},
16: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605260138418881.mp3",
text: '气压计数据异常'
},
32: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605293855295508.mp3",
text: '普通gps1数据异常'
},
64: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605323053728906.mp3",
text: '普通gps2数据异常'
},
128: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605358695187779.mp3",
text: 'RTK板卡数据异常'
},
256: {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746605386457986166.mp3",
text: 'RTK和磁偏航角差异过大'
},
};

18
out/production/uav-edu-mp/src/config/fcSystemStatus.js

@ -0,0 +1,18 @@
/**
* 飞控系统状态
*/
// 依据https://mavlink.io/en/messages/common.html#MAV_STATE
export const FC_SYSTEM_STATUS = new Map([
['MAV_STATE_UNINIT', '未初始化'],
['MAV_STATE_BOOT', '启动中'],
['MAV_STATE_CALIBRATING', '校准中'],
['MAV_STATE_STANDBY', '已上锁'],
['MAV_STATE_ACTIVE', '已解锁'],
['MAV_STATE_CRITICAL', '临界状态'],
['MAV_STATE_EMERGENCY', '紧急状态'],
['MAV_STATE_POWEROFF', '关闭'],
['MAV_STATE_FLIGHT_TERMINATION', '终止中'],
]);
export const FC_SYSTEM_STATUS_LABEL = [...FC_SYSTEM_STATUS.values()];

24
out/production/uav-edu-mp/src/config/flyModeMap.js

@ -0,0 +1,24 @@
/**
* 飞行模式
*/
export const FLY_MODE = {
3: '姿态模式',
4: '定点模式',
10: '自动起飞',
11: '自动悬停',
12: '自动返航',
15: '自动巡航',
18: '指点飞行',
19: '降落',
20: '迫降',
21: '跟随',
23: '航点环绕',
24: '动平台起飞',
25: '动平台降落',
26: '自主避障',
27: '控制',
28: '队形编队',
};
export default {};

41
out/production/uav-edu-mp/src/config/gpsFixTypeMap.js

@ -0,0 +1,41 @@
/**
* GPS定位类型
*/
// 适用于GPS_INPUT(https://mavlink.io/en/messages/common.html#GPS_INPUT)
export const GPS_FIX_TYPE = {
0: '无GPS',
1: '无定位',
2: '经纬定位',
3: '经纬高定位',
4: '差分GPS',
5: 'RTK定位',
};
// 适用于GPS2_RAW(https://mavlink.io/en/messages/common.html#GPS2_RAW)
// 依据https://mavlink.io/en/messages/common.html#GPS_FIX_TYPE
export const GPS_FIX_TYPE2 = new Map([
['GPS_FIX_TYPE_NO_GPS', '无GPS'],
['GPS_FIX_TYPE_NO_FIX', '无定位'],
['GPS_FIX_TYPE_2D_FIX', '经纬定位'],
['GPS_FIX_TYPE_3D_FIX', '经纬高定位'],
['GPS_FIX_TYPE_DGPS', '差分GPS'],
['GPS_FIX_TYPE_RTK_FLOAT', 'RTK浮点解'],
['GPS_FIX_TYPE_RTK_FIXED', 'RTK固定解'],
['GPS_FIX_TYPE_STATIC', '静态固定定位'],
['GPS_FIX_TYPE_PPP', '精密单点定位'],
]);
export const GPS_FIX_TYPE2_LABEL = [...GPS_FIX_TYPE2.values()];
export const GPS_FIX_TYPE3 = {
0: '无GPS',
1: '无定位',
2: '经纬定位',
3: '经纬高定位',
4: '差分GPS',
5: 'RTK浮点解',
6: 'RTK固定解',
7: '静态固定定位',
8: '精密单点定位',
}

11
out/production/uav-edu-mp/src/config/stageMap.js

@ -0,0 +1,11 @@
export const STAGE_MAP = {
spin: {
text: '自旋',
},
shape8: {
text: '八字飞行',
},
hover: {
text: '悬停',
},
}

187
out/production/uav-edu-mp/src/config/tipTextMap.js

@ -0,0 +1,187 @@
export const TIP_TEXT = {
"first_start_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746782401303726331.mp3",
// fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745815890849334086.mp3",
text: "考试开始",
},
"height_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745815948315941248.mp3",
text: "停至高度大于1.5米,小于5米",
},
"enter_center_time_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745815671188591010.mp3",
text: "请进入中心桶,一分钟倒计时",
},
"enter_center_time_out": {
icon: 'stopwatch',
key: 'outTime',
unit: 's',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816018284105950.mp3",
text: "操作失败,进入中心桶已超时"
},
"hover_tail_hints":{
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816051153798301.mp3",
text: "请悬停对尾"
},
"second_start_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816095288763692.mp3",
text: "第2次考试开始"
},
"third_start_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816133723481929.mp3",
text: "第3次考试开始"
},
"height_hints2": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816207762815424.mp3",
text: "请升高至1.5米"
},
"fail_horizontal_large": {
icon: 'offset-h',
key: 'horizontal',
unit: 'm',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816356733350242.mp3",
text: "操作失败,水平偏差过大"
},
"fail_vertical_large": {
icon: 'offset-v',
key: 'vertical',
unit: 'm',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816464302514672.mp3",
text: "操作失败,垂直偏差过大",
},
"fail_direction_reverse": {
icon: 'rotate360',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816482427120413.mp3",
text: "操作失败,无人机回转"
},
"fail_spin_time_out": {
icon: 'stopwatch',
key: 'outTime',
unit: 's',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816508442043575.mp3",
text: "操作失败,自旋已超时"
},
"spin_first_clock_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816546262459206.mp3",
text: "第一圈完毕,请顺时针旋转"
},
"spin_first_rclock_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816563363697085.mp3",
text: "第一圈完毕,请逆时针旋转"
},
"spin_fail_clock_time_out": {
icon: 'stopwatch',
key: 'outTime',
unit: 's',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816583793120597.mp3",
text: "操作失败,顺时针自旋已超时"
},
"spin_fail_rclock_time_out": {
icon: 'stopwatch',
key: 'outTime',
unit: 's',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816602031257891.mp3",
text: "操作失败,逆时针自旋已超时"
},
"flight_8_start_hints": {
fileUrl: 'http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745814185096733360.mp3',
text: "8字飞行开始,3分钟倒计时",
},
"fail_8_time_out": {
icon: 'stopwatch',
key: 'outTime',
unit: 's',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816626528127642.mp3",
text: "操作失败,8字飞行已超时"
},
"fail_speed_large": {
icon: 'tangent-speed',
key: 'speed',
unit: 'm/s',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816721721001188.mp3",
text: "操作失败,切线速度过高"
},
"fail_speed_low": {
icon: 'tangent-speed',
key: 'speed',
unit: 'm/s',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816653069185482.mp3",
text: "操作失败,切线速度过低"
},
"fail_yaw_large": {
icon: 'tangent-angle',
key: 'angle',
unit: '°',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816746696830596.mp3",
text: "操作失败,航向偏差过大"
},
"exam_pass_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816765576959891.mp3",
text: "考试通过"
},
"landing": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816825729131461.mp3",
text: "请降落到圈外"
},
"pasue_time_out": {
key: 'outTime',
unit: 's',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816842033628886.mp3",
text: "暂停已超时"
},
"exam_fail_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745816864370674197.mp3",
text: "考试未通过"
},
"spin_start_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745822423415207696.mp3",
text: '自旋开始,一分钟倒计时'
},
"enter_90_hints": {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745822448671992601.mp3",
text: '请进入中心桶, 90秒倒计时'
},
'angle_speed_low': {
icon: 'angle-speed',
key: 'angleSpeed',
unit: '°/s',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/04/audio_1745829747486669352.mp3",
text: '操作失败,角速度过低'
},
'fly_mode_error': {
icon: 'stability',
// key: 'angleSpeed',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746690010348557569.mp3",
text: '操作失败,姿态模式错误',
},
'return_start_hints': {
// icon: 'stability',
// key: 'angleSpeed',
// fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1746690010348557569.mp3",
text: '开始返航',
},
'fail_return_time_out': {
key: 'outTime',
unit: 's',
text: '操作失败,返航超时',
},
'fail_return_yaw_large': {
text: '操作失败,返航角偏差过大',
},
'fail_return_vertical_large': {
text: '操作失败,垂直偏差过大',
},
'pause_start': {
fileUrl: 'http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1748313316155342077.mp3',
text: '考试自动暂停中',
},
'pause_end': {
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1748313445226119867.mp3",
text: '暂停已恢复',
},
'fail_8_direction': {
icon: 'rotate360',
fileUrl: "http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/audio/2025/05/audio_1748594886504325922.mp3",
text: '操作失败,飞行方向错误',
}
}

80
out/production/uav-edu-mp/src/config/urls.js

@ -0,0 +1,80 @@
/**
* 接口地址列表
*/
import { buildURL } from '../utils/helpers';
const { TARO_APP_API: BASE_URL } = process.env; // 获取环境变量
// const { TARO_APP_API: BASE_URL } = import.meta; // 获取环境变量
// console.log(import.env);
// 上传
export const UPLOAD = `${BASE_URL}/train/v1/files/upload`;
// 刷新token
export const REFRESH_TOKEN = `${BASE_URL}/user/tokens/refreshToken`;
// 用户登录
export const LOGIN_WITH_PASSWORD = `${BASE_URL}/user/user/token`;
export const STUDENT_LOGIN_WITH_PASSWORD = `${BASE_URL}/user/student/token`;
export const WECHAT_AUTH_URL = `${BASE_URL}/user/wechat/mini/auth`;
export const GET_WECHAT_USERINFO = `${BASE_URL}/user/wechat/mini/getWxUserInfo`;
//学生信息
export const UPDATE_STUDENT = (id) => buildURL(`${BASE_URL}/user/v1/students/{id}`, id);
//人员信息
export const UPDATE_MEMBER = (id) => buildURL(`${BASE_URL}/user/v1/users/{id}`, id);
// 讲评管理
export const GET_EVALUATION_LIST = `${BASE_URL}/train/v1/evaluations`;
export const CREATE_EVALUATION = `${BASE_URL}/train/v1/evaluations`;
export const UPDATE_EVALUATION = `${BASE_URL}/train/v1/evaluations`;
export const GET_EVALUATION_DETAIL = (id) => buildURL(`${BASE_URL}/train/v1/evaluations/{id}`, id);
export const REPLY_EVALUATION = `${BASE_URL}/train/v1/evaluations/reply`;
// 实践飞行管理
export const GET_FLIGHT_LIST = `${BASE_URL}/train/v1/flightRecords`;
export const GET_FLIGHT_DETAIL = (id) => buildURL(`${BASE_URL}/train/v1/flightRecords/{id}`, id);
export const GET_FLIGHT_TRACKS = (id) => buildURL(`${BASE_URL}/train/v1/flightRecords/{id}/tracks`, id);
export const STUDENT_BIND_DRONE = `${BASE_URL}/user/v1/drones/qrCode/scan`;
// 航线管理
export const GET_ROUTE_PLAN_LIST = `${BASE_URL}/train/v1/routePlans`;
export const GET_ROUTE_PLAN_DETAIL = (id) => buildURL(`${BASE_URL}/train/v1/routePlans/{id}`, id);
// 应急返航管理
export const GET_RETURN_TRIP_LIST = `${BASE_URL}/train/v1/returnTrips`;
export const GET_RETURN_TRIP_DETAIL = (id) => buildURL(`${BASE_URL}/train/v1/returnTrips/{id}`, id);
export const GET_RETURN_TRIP_TRACKS = (id) => buildURL(`${BASE_URL}/train/v1/returnTrips/{id}/tracks`, id);
// 环境参数管理
export const GET_ENV_LIST = `${BASE_URL}/train/v1/envs`;
// 考试标准管理
export const GET_EXAM_LIST = `${BASE_URL}/train/v1/exams`;
// ws 飞机实时数据
// export const GET_DRONE_REAL_TIME_DATA = `${WS_BASE_URL}/data/ws`;
export const GET_DRONE_REAL_TIME_DATA = `wss://uavedu.jiagutech.com/api/data/ws`;
// 场地管理
export const GET_AIRFIELDS_LIST = `${BASE_URL}/train/v1/airfields`;
export const GET_AIRFIELDS_DETAIL = (id) => buildURL(`${BASE_URL}/train/v1/airfields/{id}`, id);
export const CREATE_AIRFIELD = `${BASE_URL}/train/v1/airfields`
export const GET_AIRFIELDS_OF_STUDENT = `${BASE_URL}/train/v1/airfields/student/current`;
export const UPDATE_AIRFIELD = (id) => buildURL(`${BASE_URL}/train/v1/airfields/{id}`, id)
export const DELETE_AIRFIELD = (id) => buildURL(`${BASE_URL}/train/v1/airfields/{id}`, id)
export const AIRFIELD_BIND_CLASS = `${BASE_URL}/train/v1/airfields/class/airfield`;
// 无人机
export const GET_DRONE_LIST = `${BASE_URL}/user/v1/drones`;
// 班级
export const GET_CLASS_LIST = `${BASE_URL}/user/v1/classes`
// 执照等级
export const GET_LICENSE_GRADES_LIST = `${BASE_URL}/user/licenseGrades`;
// 微信解绑 - 机构人员
export const UNBIND_USER = `${BASE_URL}/user/v1/users/unbind`;
// 微信解绑 - 学生
export const UNBIND_STUDENT = `${BASE_URL}/user/v1/students/unbind`;

165
out/production/uav-edu-mp/src/core/FormData.js

@ -0,0 +1,165 @@
// 文件 formData.js
import mimeMap from './mimeMap';
import Taro from "@tarojs/taro";
function FormData(){
let fileManager = Taro.getFileSystemManager();
let data = {};
let files = [];
this.clearCacheData = () => {
data = {};
files = [];
}
this.append = (name, value)=>{
data[name] = value;
return true;
}
this.appendFile = (name, path, fileName)=>{
let buffer = fileManager.readFileSync(path);
if(Object.prototype.toString.call(buffer).indexOf("ArrayBuffer") < 0){
return false;
}
if(!fileName){
fileName = getFileNameFromPath(path);
}
files.push({
name: name,
buffer: buffer,
fileName: fileName
});
return true;
}
this.getData = ()=>convert(data, files)
}
function getFileNameFromPath(path){
let idx=path.lastIndexOf("/");
return path.substr(idx+1);
}
function convert(data, files){
let boundaryKey = 'wxmpFormBoundary' + randString(); // 数据分割符,一般是随机的字符串
let boundary = '--' + boundaryKey;
let endBoundary = boundary + '--';
let postArray = [];
//拼接参数
if(data && Object.prototype.toString.call(data) == "[object Object]"){
for(let key in data){
postArray = postArray.concat(formDataArray(boundary, key, data[key]));
}
}
//拼接文件
if(files && Object.prototype.toString.call(files) == "[object Array]"){
for(let i in files){
let file = files[i];
postArray = postArray.concat(formDataArray(boundary, file.name, file.buffer, file.fileName));
}
}
//结尾
let endBoundaryArray = [];
endBoundaryArray.push(...endBoundary.toUtf8Bytes());
postArray = postArray.concat(endBoundaryArray);
return {
contentType: 'multipart/form-data; boundary=' + boundaryKey,
buffer: new Uint8Array(postArray).buffer
}
}
function randString() {
let res = "";
for (let i = 0; i < 17; i++) {
let n = parseInt(Math.random() * 62);
if (n <= 9) {
res += n;
}
else if (n <= 35) {
res += String.fromCharCode(n + 55);
}
else {
res += String.fromCharCode(n + 61);
}
}
return res;
}
function formDataArray(boundary, name, value, fileName){
let dataString = '';
let isFile = !!fileName;
dataString += boundary + '\r\n';
dataString += 'Content-Disposition: form-data; name="' + name + '"';
if (isFile){
dataString += '; filename="' + fileName + '"' + '\r\n';
dataString += 'Content-Type: ' + getFileMime(fileName) + '\r\n\r\n';
}
else{
dataString += '\r\n\r\n';
dataString += value;
}
let dataArray = [];
dataArray.push(...dataString.toUtf8Bytes());
if (isFile) {
let fileArray = new Uint8Array(value);
dataArray = dataArray.concat(Array.prototype.slice.call(fileArray));
}
dataArray.push(..."\r".toUtf8Bytes());
dataArray.push(..."\n".toUtf8Bytes());
return dataArray;
}
function getFileMime(fileName){
let idx = fileName.lastIndexOf(".");
let mime = mimeMap[fileName.substr(idx)];
return mime?mime:"application/octet-stream"
}
String.prototype.toUtf8Bytes = function(){
var str = this;
var bytes = [];
for (var i = 0; i < str.length; i++) {
bytes.push(...str.utf8CodeAt(i));
if (str.codePointAt(i) > 0xffff) {
i++;
}
}
return bytes;
}
String.prototype.utf8CodeAt = function(i) {
var str = this;
var out = [], p = 0;
var c = str.charCodeAt(i);
if (c < 128) {
out[p++] = c;
} else if (c < 2048) {
out[p++] = (c >> 6) | 192;
out[p++] = (c & 63) | 128;
} else if (
((c & 0xFC00) == 0xD800) && (i + 1) < str.length &&
((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) {
// Surrogate Pair
c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF);
out[p++] = (c >> 18) | 240;
out[p++] = ((c >> 12) & 63) | 128;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
} else {
out[p++] = (c >> 12) | 224;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
}
return out;
};
export default FormData;

108
out/production/uav-edu-mp/src/core/SimpleAudioManager.js

@ -0,0 +1,108 @@
import Taro from "@tarojs/taro";
class SimpleAudioManager {
// audio;
timer;
constructor() {
this.audio = Taro.createInnerAudioContext({ useWebAudioImplement: true });
// this.audio = Taro.getBackgroundAudioManager();
this.queue = []; // 待播放队列(仅存同优先级)
this.currentPriority = 0; // 当前播放优先级
// 监听播放结束
this.audio.onEnded(() => this.playNext());
this.audio.onError(() => this.playNext());
}
check() {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.playNext, 5000);
}
// 添加播放请求(核心逻辑)
play(url, priority = 0) {
console.log('play:url', url);
console.log('上一个优先级', this.currentPriority, '本次的优先级', priority);
// 情况1:更高优先级 → 立即打断并清空
if (priority > this.currentPriority) {
// this.audio.stop();
console.log('清空队列');
this.queue = [];
this.currentPriority = priority;
if (this.audio) {
this.audio.destroy();
this.audio = null;
this.audio = Taro.createInnerAudioContext({ useWebAudioImplement: true });
// 监听播放结束
this.audio.onEnded(() => this.playNext());
this.audio.onError(() => this.playNext());
//
this.audio.src = url;
this.audio.play();
this.check();
}
return;
}
// 情况2:同优先级 → 加入队列等待
if (priority === this.currentPriority) {
this.queue.push(url);
console.log('排队', [...(this.queue || [])]);
// return;
}
// if (!this.queue.length) {
// this.currentPriority = 0;
// }
// 情况3:低优先级 → 直接忽略
// 情况4:低优先级 → 但是queue 为空
// if (!this.queue.length) {
// // this.queue = [];
// this.currentPriority = priority;
// this.audio.src = url;
// this.audio.play();
// }
}
// 播放下一个(仅在同优先级队列有效)
playNext() {
if (this.queue?.length > 0) {
// 继续播放同优先级队列
// this.audio.src = this.queue.shift();
// this.audio.play();
if (this.audio) {
this.audio.destroy();
this.audio = null;
this.audio = Taro.createInnerAudioContext({ useWebAudioImplement: true });
// 监听播放结束
this.audio.onEnded(() => this.playNext());
this.audio.onError(() => this.playNext());
//
this.audio.src = this.queue?.shift();
this.audio.play();
this.check();
console.log('异步操作:排队播放中,剩余数量', this.queue?.length, [...(this.queue || [])]);
}
} else {
console.log('异步操作:排队播放完毕', [...(this.queue || [])]);
// this.queue = [];
// 队列清空,重置优先级
this.currentPriority = 0;
}
}
destroy() {
this.queue = [];
this.currentPriority = 0;
if (this.audio) {
this.audio.destroy();
this.audio = null
this.queue = []; // 待播放队列(仅存同优先级)
this.currentPriority = 0; // 当前播放优先级
console.log('audioManager.destroy');
}
}
}
export default SimpleAudioManager;

92
out/production/uav-edu-mp/src/core/helper/markHelper.js

@ -0,0 +1,92 @@
import { Point, PointerEvent } from "@leafer-ui/miniapp";
import CLASSIFY_MAP from '../../../config/classifyMap';
// import { ref } from "vue";
let rapes;
let handles;
let beaks;
function init(data = { rapes: [], handles: [], beaks: [] }) {
rapes = data.rapes;
handles = data.handles;
beaks = data.beaks;
}
function allMarkOnListen(event = PointerEvent.TAP, ftn = () => {}) {
rapes.forEach(rapeItem => {
rapeItem.on(event, ftn);
});
beaks.forEach(beakItem => {
beakItem.on(event, ftn);
});
handles.forEach(handleItem => {
handleItem.on(event, ftn);
});
}
function allMarkOffListen(event = PointerEvent.TAP, ftn = () => {}) {
rapes.forEach(rapeItem => {
rapeItem.off(event, ftn);
});
beaks.forEach(beakItem => {
beakItem.off(event, ftn);
});
handles.forEach(handleItem => {
handleItem.off(event, ftn);
});
}
function setRapesVisible(val = true) {
rapes.forEach(rapeItem => {
rapeItem.set({
visible: val,
});
});
}
function setBeaksVisible(val = true) {
beaks.forEach(beakItem => {
beakItem.set({
visible: val,
});
});
}
function setHandlesVisible(val = true) {
handles.forEach(handleItem => {
handleItem.set({
visible: val,
});
});
}
function InfoInduction(e, ratio = 0) {
const { extra: { classify, points } } = e?.current || {};
const pointStart = points[0];
const pointEnd = points[2];
const point = new Point()
const pixel = point.set(pointStart[0], pointStart[1]).getDistance({ x: pointEnd[0], y: pointEnd[1] }); // 100
const { [classify]: { zh: name } } = CLASSIFY_MAP;
const distance = ratio ? +((pixel / ratio).toFixed(1)) : 0;
//
// console.log('pixel', pixel);
// console.log('distance', distance);
// console.log('name', name);
//
return {
name,
pixel,
distance,
}
}
export default {
init,
allMarkOnListen,
allMarkOffListen,
setRapesVisible,
setBeaksVisible,
setHandlesVisible,
InfoInduction,
}

281
out/production/uav-edu-mp/src/core/helper/pinHelper.js

@ -0,0 +1,281 @@
import { DragEvent, Path, Point, PointerEvent, Text, ZoomEvent } from "@leafer-ui/miniapp";
import { computed, ref } from "vue";
const pinOption = {
// scale: 0.03,
around: 'bottom',
path: "M464.1 461.4v-240c0-5-1.6-9.1-4.8-12.3-3.2-3.2-7.3-4.8-12.3-4.8-5 0-9.1 1.6-12.3 4.8-3.2 3.2-4.8 7.3-4.8 12.3v240c0 5 1.6 9.1 4.8 12.3 3.2 3.2 7.3 4.8 12.3 4.8 5 0 9.1-1.6 12.3-4.8 3.2-3.1 4.8-7.2 4.8-12.3z m360 188.6c0 9.3-3.4 17.3-10.1 24.1-6.8 6.8-14.8 10.2-24.1 10.1H560l-27.3 258.7c-0.7 4.3-2.6 8-5.6 11-3 3-6.7 4.5-11 4.5h-0.5c-9.6 0-15.4-4.8-17.1-14.5l-40.7-259.8H241.3c-9.3 0-17.3-3.4-24.1-10.1-6.8-6.8-10.2-14.8-10.1-24.1 0-43.9 14-83.5 42-118.6 28-35.1 59.7-52.7 95.1-52.8V204.2c-18.6 0-34.6-6.8-48.2-20.4-13.6-13.6-20.4-29.6-20.4-48.2s6.8-34.6 20.4-48.2C309.6 73.8 325.6 67 344.2 67H687c18.6 0 34.6 6.8 48.2 20.4 13.6 13.6 20.4 29.6 20.4 48.2s-6.8 34.6-20.4 48.2c-13.6 13.6-29.6 20.4-48.2 20.4v274.3c35.3 0 67 17.6 95.1 52.8 28 35.2 42 74.7 42 118.7z m0 0",
fill: 'rgba(255,108,0,0.9)',
};
const lineOption = {
stroke: '#00ffff',
strokeWidth: 10,
}
const textOption = {
around: 'center',
fontSize: 100,
fill: 'rgb(175,0,0)',
text: `点击输入实际长度`,
fontWeight: 'bold',
padding: [0, 0, 200]
}
let pinS;
let line;
let pinE;
let text;
let group;
const length = ref('0');
const ratio = computed(() => {
if (!pinE) return 0;
if (length.value === '0') return 0;
const pixel = new Point().set(pinS.x, pinS.y).getDistance({ x: pinE.x, y: pinE.y });
return +((pixel / +length.value).toFixed(1));
});
function installGroup(g = {}) {
group = g;
}
function creatPinS(option = {}) {
if (pinS) return;
pinS = new Path({ ...pinOption, ...option });
group.add(pinS);
}
function creatLine(option = {}) {
if (pinE) return;
console.log('optionS', option);
line = new Path({
path: [1, option.sx, option.sy, 2, option.ex, option.ey],
...lineOption,
})
group.add(line);
}
function moveLine(option = {}) {
if (pinE) return;
line.set({
path: [1, option.sx, option.sy, 2, option.ex, option.ey],
})
}
function creatPinE(option = {}) {
if (pinE) return;
pinE = new Path({ ...pinOption, ...option, className: 'pinE' });
group.add(pinE);
//
pinS.set({ draggable: true });
pinE.set({ draggable: true });
_creatComplete();
}
const eventPool = {};
function textTapBack(ftn = () => {}) {
eventPool.textTapBack = ftn;
if (text) {
text.on(PointerEvent.TAP, eventPool?.textTapBack);
}
}
function creatText(option = {}) {
if (text) return;
const textPoint = new Point().set(pinS.x, pinS.y).getCenter({ x: pinE.x, y: pinE.y });
const rotation = new Point().set(pinS.x, pinS.y).getRotation({ x: pinS.x, y: pinS.y }, { x: pinE.x, y: pinE.y });
// scale: 0.1 / e.current.scale,
text = new Text({
x: textPoint.x,
y: textPoint.y,
rotation: rotation,
...option,
...textOption,
});
group.add(text);
//
text.on(PointerEvent.TAP, (eventPool?.textTapBack || function () {}));
}
function _pinSDragCallBack(e) {
const point = e.getLocal();
pinS.set({ x: point.x, y: point.y });
line.set({ path: [1, pinS.x, pinS.y, 2, pinE.x, pinE.y] });
const textPoint = new Point().set(pinS.x, pinS.y).getCenter({ x: pinE.x, y: pinE.y });
const rotation = new Point().set(pinS.x, pinS.y).getRotation({ x: pinS.x, y: pinS.y }, { x: pinE.x, y: pinE.y });
const pixel = new Point().set(pinS.x, pinS.y).getDistance({ x: pinE.x, y: pinE.y });
// console.log('rotation', rotation, { x: pinS.x, y: pinS.y }, { x: pinE.x, y: pinE.y });
const l = ratio.value ? +((pixel / ratio.value).toFixed(1)) : 0;
text.set({
x: textPoint.x,
y: textPoint.y,
rotation,
text: l ? `${l}cm` : '点击输入实际长度',
});
length.value = `${l}`;
}
function _pinEDragCallBack(e) {
const point = e.getLocal();
pinE.set({ x: point.x, y: point.y });
line.set({ path: [1, pinS.x, pinS.y, 2, pinE.x, pinE.y] });
const textPoint = new Point().set(pinS.x, pinS.y).getCenter({ x: pinE.x, y: pinE.y });
const rotation = new Point().set(pinS.x, pinS.y).getRotation({ x: pinS.x, y: pinS.y }, { x: pinE.x, y: pinE.y });
const pixel = new Point().set(pinS.x, pinS.y).getDistance({ x: pinE.x, y: pinE.y });
// console.log('rotation', rotation, { x: pinS.x, y: pinS.y }, { x: pinE.x, y: pinE.y });
const l = ratio.value ? +((pixel / ratio.value).toFixed(1)) : 0;
text.set({
x: textPoint.x,
y: textPoint.y,
rotation,
text: l ? `${l}cm` : '点击输入实际长度',
});
length.value = `${l}`;
}
function setLength(val) {
length.value = `${val}`;
if (text) {
text.set({
text: `${val}cm`,
})
}
}
function _groupZoom(e) {
if (pinS) {
pinS.set({
scale: 0.03 / e.current.scale,
})
}
if (pinE) {
pinE.set({
scale: 0.03 / e.current.scale,
})
}
if (text) {
text.set({
scale: 0.1 / e.current.scale,
})
}
if (line) {
line.set({
strokeWidth: 5 / group.scale,
});
}
}
function _creatComplete() {
pinS.on(DragEvent.DRAG, _pinSDragCallBack);
pinE.on(DragEvent.DRAG, _pinEDragCallBack);
group.on(ZoomEvent.ZOOM, _groupZoom)
}
function _offDragEvent() {
pinS.off(DragEvent.DRAG, _pinSDragCallBack);
pinE.off(DragEvent.DRAG, _pinEDragCallBack);
}
function _onDragEvent() {
pinS.on(DragEvent.DRAG, _pinSDragCallBack);
pinE.on(DragEvent.DRAG, _pinEDragCallBack);
}
function pinDragAble(val = false) {
if (pinS) {
pinS.set({ draggable: val });
}
if (pinE) {
pinE.set({ draggable: val });
}
if (pinS && pinE) {
if (val) _onDragEvent();
if (!val) _offDragEvent();
}
console.log('val', val, pinS, pinE);
}
function offTextTapBack() {
if (text) {
text.off(PointerEvent.TAP, (eventPool?.textTapBack || function () {}));
}
}
function getScaleBarInfo() {
return {
points: [[pinS.x, pinS.y], [pinE.x, pinE.y]],
scale: ratio.value,
size: length.value,
}
}
function showScale(option = {}, groupScale = 1) {
const { points, scale, size } = option;
const pinSxy = { x: points[0][0], y: points[0][1] };
const pinExy = { x: points[1][0], y: points[1][1] };
pinS = new Path({ ...pinOption, x: pinSxy.x, y: pinSxy.y, scale: 0.03 / groupScale, draggable: false });
pinE = new Path({ ...pinOption, x: pinExy.x, y: pinExy.y, scale: 0.03 / groupScale, draggable: false, className: 'pinE' });
line = new Path({
path: [1, pinSxy.x, pinSxy.y, 2, pinExy.x, pinExy.y],
...lineOption,
strokeWidth: 5 / groupScale,
});
const textPoint = new Point().set(pinSxy.x, pinSxy.y).getCenter({ x: pinExy.x, y: pinExy.y });
const rotation = new Point().set(pinSxy.x, pinSxy.y).getRotation({ x: pinSxy.x, y: pinSxy.y }, { x: pinExy.x, y: pinExy.y });
// scale: 0.1 / e.current.scale,
text = new Text({
x: textPoint.x,
y: textPoint.y,
rotation: rotation,
// ...option,
...textOption,
scale: 0.1 / groupScale,
text: `${size}cm`,
});
//
text.on(PointerEvent.TAP, (eventPool?.textTapBack || function () {}));
group.add(pinS);
group.add(pinE);
group.add(line);
group.add(text);
length.value = `${size}`;
group.on(ZoomEvent.ZOOM, _groupZoom)
}
function clearPin() {
if (pinE) {
_offDragEvent();
group.off(ZoomEvent.ZOOM, _groupZoom);
group.remove(pinS, true);
group.remove(pinE, true);
group.remove(line, true);
group.remove(text, true);
pinS = undefined;
pinE = undefined;
line = undefined;
text = undefined;
length.value = '0';
}
}
export default {
installGroup,
creatPinS,
creatLine,
moveLine,
creatPinE,
creatText,
textTapBack,
length,
ratio,
setLength,
pinDragAble,
offTextTapBack,
getScaleBarInfo,
showScale,
clearPin,
};

273
out/production/uav-edu-mp/src/core/helper/sceneHelper.js

@ -0,0 +1,273 @@
import { Group, Image, ImageEvent, Leafer, Line, Path, Point, PointerEvent } from "@leafer-ui/miniapp";
import CLASSIFY_MAP from '../../../config/classifyMap';
import MarkHelper from './markHelper';
import PinHelper from './pinHelper';
import Taro from "@tarojs/taro";
// import { ref } from "vue";
let leafer;
let group;
let image;
function init(option = {}) {
leafer = new Leafer({ ...option });
return leafer;
}
function setGroup(option = {}) {
group = new Group({ ...option });
leafer.add(group);
leafer.zoomLayer = group;
PinHelper.installGroup(group);
return group;
}
let scale = 0.1;
function groupSetImage(option = {}, callBackFtn = () => {}) {
image = new Image({ ...option });
group.add(image);
image.once(ImageEvent.LOADED, function (e) {
const res = Taro.getSystemInfoSync();
const scale = res.windowWidth / e.current.width;
groupFill(scale);
callBackFtn(scale);
})
return image;
}
function groupSetMark(data = [], option = {}) {
const rapes = [];
const handles = [];
const beaks = [];
data.forEach(({ classify, name, points }) => {
const { [classify]: { color: stroke, strokeWidth } } = CLASSIFY_MAP;
const point1 = { x: points[0][0], y: points[0][1] };
const point2 = { x: points[1][0], y: points[1][1] };
const point3 = { x: points[2][0], y: points[2][1] };
const point4 = { x: points[3][0], y: points[3][1] };
const item = new Path({
className: name,
extra: { classify, name, points },
path: [1, point1.x, point1.y, 2, point2.x, point2.y, 2, point3.x, point3.y, 2, point4.x, point4.y, 11],
stroke,
strokeWidth,
fill: 'rgba(255,255,255,0)'
})
group.add(item);
// 分类
const { [classify]: list } = { 0: rapes, 1: beaks, 2: handles };
list.push(item);
});
MarkHelper.init({ rapes, handles, beaks });
}
function getMarkList() {
const rapesList = group.find('.rape');
const handlesList = group.find('.handle');
const beaksList = group.find('.beak');
return { rapesList, handlesList, beaksList };
}
function setMarkTapListen(ftn = () => {}) {
MarkHelper.allMarkOnListen(PointerEvent.TAP, ftn);
}
function offMarkTapListen(ftn = () => {}) {
MarkHelper.allMarkOffListen(PointerEvent.TAP, ftn);
}
function groupFill(scale = 0.1) {
group.scaleOf('left', scale);
group.set({
y: 150
});
}
function groupOnDownListen(ftn = () => {}) {
group.on(PointerEvent.DOWN, ftn);
}
function groupOffDownListen(ftn = () => {}) {
group.off(PointerEvent.DOWN, ftn);
}
function groupRemoveChild(child = {}) {
group.remove(child);
}
function groupOnMoveListen(ftn = () => {}) {
group.on(PointerEvent.MOVE, ftn);
}
function groupOnUpListen(ftn = () => {}) {
group.on(PointerEvent.UP, ftn);
}
let sx;
let sy;
function _moveLineEPoint(e) {
const point = { x: e.x, y: e.y };
image.worldToLocal(point);
const lineOption = { sx, sy, ex: point.x, ey: point.y };
PinHelper.moveLine({ ...lineOption });
}
function _creatPinSPoint(e) {
const point = { x: e.x, y: e.y };
image.worldToLocal(point);
sx = point.x;
sy = point.y;
PinHelper.creatPinS({ ...point, scale: 0.03 / e.current.scale, });
const lineOption = { sx: point.x, sy: point.y, ex: point.x, ey: point.y };
PinHelper.creatLine({ ...lineOption });
}
// let pinTextTapCallBack = () => {};
function _endPinEPoint(e) {
const point = { x: e.x, y: e.y };
image.worldToLocal(point);
const lineOption = { sx, sy, ex: point.x, ey: point.y };
PinHelper.moveLine({ ...lineOption });
PinHelper.creatPinE({ ...point, scale: 0.03 / e.current.scale });
PinHelper.creatText({ scale: 0.1 / e.current.scale });
group.off(PointerEvent.DOWN, _creatPinSPoint);
group.off(PointerEvent.MOVE, _moveLineEPoint);
group.off(PointerEvent.UP, _endPinEPoint);
}
function groupCreatPin() {
group.on(PointerEvent.DOWN, _creatPinSPoint);
group.on(PointerEvent.MOVE, _moveLineEPoint);
group.on(PointerEvent.UP, _endPinEPoint);
}
function calculateNewPoints(A, B, d) {
// 计算方向向量
let directionVector = { x: B.x - A.x, y: B.y - A.y };
// 计算方向向量的长度
let length = Math.hypot(directionVector.x, directionVector.y);
// 归一化方向向量
let unitDirectionVector = { x: directionVector.x / length, y: directionVector.y / length };
// 计算延长线段的向量
// let extensionVector = { x: unitDirectionVector.x * d, y: unitDirectionVector.y * d };
// 计算延长后的端点坐标
// let newPointA1 = { x: A.x - extensionVector.x, y: A.y - extensionVector.y };
// let newPointA2 = { x: A.x + extensionVector.x, y: A.y + extensionVector.y };
// let newPointB1 = { x: B.x - extensionVector.x, y: B.y - extensionVector.y };
// let newPointB2 = { x: B.x + extensionVector.x, y: B.y + extensionVector.y };
// 计算垂直于AB的单位向量
let perpendicularUnitVector = { x: -unitDirectionVector.y, y: unitDirectionVector.x };
// 计算顺时针和逆时针旋转90度后的新端点坐标
let newPointA3 = { x: A.x + perpendicularUnitVector.x * d, y: A.y + perpendicularUnitVector.y * d };
let newPointA4 = { x: A.x - perpendicularUnitVector.x * d, y: A.y - perpendicularUnitVector.y * d };
let newPointB3 = { x: B.x + perpendicularUnitVector.x * d, y: B.y + perpendicularUnitVector.y * d };
let newPointB4 = { x: B.x - perpendicularUnitVector.x * d, y: B.y - perpendicularUnitVector.y * d };
return {
// newPointsA: [newPointA1, newPointA2, newPointA3, newPointA4],
// newPointsB: [newPointB1, newPointB2, newPointB3, newPointB4],
point: [newPointA3, newPointB3, newPointB4, newPointA4],
};
}
function groupAddMark({ type = '', tapCallBack = () => {}, height = 10 }) {
group.set({ draggable: false });
let line;
let sx;
let sy;
function _addMarkStart(e) {
const point = e.getInner();
sx = point.x;
sy = point.y;
line = new Line({
points: [sx, sy, sx, sy],
stroke: '#00ffff',
strokeWidth: 10,
})
group.add(line);
}
function _moveMark(e) {
const point = e.getInner();
line.set({
points: [sx, sy, point.x, point.y],
})
}
function _doneDrawMark(e) {
const point = e.getInner();
line.set({
points: [sx, sy, point.x, point.y],
});
// 示例使用
// let A = { x: 1, y: 2 };
// let B = { x: 4, y: 6 };
// let d = 3;
let result = calculateNewPoints({ x: sx, y: sy }, { x: point.x, y: point.y }, height);
const temp = result.point;
const point1 = { x: temp[0].x, y: temp[0].y };
const point2 = { x: temp[1].x, y: temp[1].y };
const point3 = { x: temp[2].x, y: temp[2].y };
const point4 = { x: temp[3].x, y: temp[3].y };
const points = [[point1.x, point1.y], [point2.x, point2.y], [point3.x, point3.y], [point4.x, point4.y],]
const typeItem = Object.values(CLASSIFY_MAP).find(({ name }) => name === type);
const item = new Path({
className: typeItem.name,
extra: { classify: typeItem.classify, name: typeItem.name, points },
path: [1, point1.x, point1.y, 2, point2.x, point2.y, 2, point3.x, point3.y, 2, point4.x, point4.y, 11],
stroke: typeItem.color,
strokeWidth: typeItem.strokeWidth,
fill: 'rgba(255,255,255,0)'
})
group.add(item);
item.on(PointerEvent.TAP, tapCallBack);
group.off(PointerEvent.DOWN, _addMarkStart);
group.off(PointerEvent.MOVE, _moveMark)
group.off(PointerEvent.UP, _doneDrawMark)
callBackFtn();
const { rapesList, handlesList, beaksList } = getMarkList()
MarkHelper.init({ rapes: rapesList, handles: handlesList, beaks: beaksList });
group.set({ draggable: true });
group.remove(line);
}
let callBackFtn = () => {};
function done(ftn = () => {}) {
callBackFtn = ftn;
}
group.on(PointerEvent.DOWN, _addMarkStart);
group.on(PointerEvent.MOVE, _moveMark)
group.on(PointerEvent.UP, _doneDrawMark)
return {
done,
}
}
export default {
// leafer,
init,
setGroup,
groupSetImage,
groupSetMark,
setMarkTapListen,
offMarkTapListen,
groupFill,
groupOnDownListen,
groupOffDownListen,
groupRemoveChild,
getMarkList,
groupCreatPin,
groupAddMark,
}

345
out/production/uav-edu-mp/src/core/mimeMap.js

@ -0,0 +1,345 @@
export default {
"0.001": "application/x-001",
"0.323": "text/h323",
"0.907": "drawing/907",
".acp": "audio/x-mei-aac",
".aif": "audio/aiff",
".aiff": "audio/aiff",
".asa": "text/asa",
".asp": "text/asp",
".au": "audio/basic",
".awf": "application/vnd.adobe.workflow",
".bmp": "application/x-bmp",
".c4t": "application/x-c4t",
".cal": "application/x-cals",
".cdf": "application/x-netcdf",
".cel": "application/x-cel",
".cg4": "application/x-g4",
".cit": "application/x-cit",
".cml": "text/xml",
".cmx": "application/x-cmx",
".crl": "application/pkix-crl",
".csi": "application/x-csi",
".cut": "application/x-cut",
".dbm": "application/x-dbm",
".dcd": "text/xml",
".der": "application/x-x509-ca-cert",
".dib": "application/x-dib",
".doc": "application/msword",
".drw": "application/x-drw",
".dwf": "Model/vnd.dwf",
".dwg": "application/x-dwg",
".dxf": "application/x-dxf",
".emf": "application/x-emf",
".ent": "text/xml",
".eps": "application/x-ps",
".etd": "application/x-ebx",
".fax": "image/fax",
".fif": "application/fractals",
".frm": "application/x-frm",
".gbr": "application/x-gbr",
".gif": "image/gif",
".gp4": "application/x-gp4",
".hmr": "application/x-hmr",
".hpl": "application/x-hpl",
".hrf": "application/x-hrf",
".htc": "text/x-component",
".html": "text/html",
".htx": "text/html",
".ico": "image/x-icon",
".iff": "application/x-iff",
".igs": "application/x-igs",
".img": "application/x-img",
".isp": "application/x-internet-signup",
".java": "java/*",
".jpe": "image/jpeg",
".jpeg": "image/jpeg",
".jpg": "application/x-jpg",
".jsp": "text/html",
".lar": "application/x-laplayer-reg",
".lavs": "audio/x-liquid-secure",
".lmsff": "audio/x-la-lms",
".ltr": "application/x-ltr",
".m2v": "video/x-mpeg",
".m4e": "video/mpeg4",
".man": "application/x-troff-man",
".mdb": "application/msaccess",
".mfp": "application/x-shockwave-flash",
".mhtml": "message/rfc822",
".mid": "audio/mid",
".mil": "application/x-mil",
".mnd": "audio/x-musicnet-download",
".mocha": "application/x-javascript",
".mp1": "audio/mp1",
".mp2v": "video/mpeg",
".mp4": "video/mpeg4",
".mpd": "application/vnd.ms-project",
".mpeg": "video/mpg",
".mpga": "audio/rn-mpeg",
".mps": "video/x-mpeg",
".mpv": "video/mpg",
".mpw": "application/vnd.ms-project",
".mtx": "text/xml",
".net": "image/pnetvue",
".nws": "message/rfc822",
".out": "application/x-out",
".p12": "application/x-pkcs12",
".p7c": "application/pkcs7-mime",
".p7r": "application/x-pkcs7-certreqresp",
".pc5": "application/x-pc5",
".pcl": "application/x-pcl",
".pdf": "application/pdf",
".pdx": "application/vnd.adobe.pdx",
".pgl": "application/x-pgl",
".pko": "application/vnd.ms-pki.pko",
".plg": "text/html",
".plt": "application/x-plt",
".png": "application/x-png",
".ppa": "application/vnd.ms-powerpoint",
".pps": "application/vnd.ms-powerpoint",
".ppt": "application/x-ppt",
".prf": "application/pics-rules",
".prt": "application/x-prt",
".ps": "application/postscript",
".pwz": "application/vnd.ms-powerpoint",
".ra": "audio/vnd.rn-realaudio",
".ras": "application/x-ras",
".rdf": "text/xml",
".red": "application/x-red",
".rjs": "application/vnd.rn-realsystem-rjs",
".rlc": "application/x-rlc",
".rm": "application/vnd.rn-realmedia",
".rmi": "audio/mid",
".rmm": "audio/x-pn-realaudio",
".rms": "application/vnd.rn-realmedia-secure",
".rmx": "application/vnd.rn-realsystem-rmx",
".rp": "image/vnd.rn-realpix",
".rsml": "application/vnd.rn-rsml",
".rtf": "application/msword",
".rv": "video/vnd.rn-realvideo",
".sat": "application/x-sat",
".sdw": "application/x-sdw",
".slb": "application/x-slb",
".slk": "drawing/x-slk",
".smil": "application/smil",
".snd": "audio/basic",
".sor": "text/plain",
".spl": "application/futuresplash",
".ssm": "application/streamingmedia",
".stl": "application/vnd.ms-pki.stl",
".sty": "application/x-sty",
".swf": "application/x-shockwave-flash",
".tg4": "application/x-tg4",
".tif": "image/tiff",
".tiff": "image/tiff",
".top": "drawing/x-top",
".tsd": "text/xml",
".uin": "application/x-icq",
".vcf": "text/x-vcard",
".vdx": "application/vnd.visio",
".vpg": "application/x-vpeg005",
".vsd": "application/x-vsd",
".vst": "application/vnd.visio",
".vsw": "application/vnd.visio",
".vtx": "application/vnd.visio",
".wav": "audio/wav",
".wb1": "application/x-wb1",
".wb3": "application/x-wb3",
".wiz": "application/msword",
".wk4": "application/x-wk4",
".wks": "application/x-wks",
".wma": "audio/x-ms-wma",
".wmf": "application/x-wmf",
".wmv": "video/x-ms-wmv",
".wmz": "application/x-ms-wmz",
".wpd": "application/x-wpd",
".wpl": "application/vnd.ms-wpl",
".wr1": "application/x-wr1",
".wrk": "application/x-wrk",
".ws2": "application/x-ws",
".wsdl": "text/xml",
".xdp": "application/vnd.adobe.xdp",
".xfd": "application/vnd.adobe.xfd",
".xhtml": "text/html",
".xls": "application/x-xls",
".xml": "text/xml",
".xq": "text/xml",
".xquery": "text/xml",
".xsl": "text/xml",
".xwd": "application/x-xwd",
".sis": "application/vnd.symbian.install",
".x_t": "application/x-x_t",
".apk": "application/vnd.android.package-archive",
"0.301": "application/x-301",
"0.906": "application/x-906",
".a11": "application/x-a11",
".ai": "application/postscript",
".aifc": "audio/aiff",
".anv": "application/x-anv",
".asf": "video/x-ms-asf",
".asx": "video/x-ms-asf",
".avi": "video/avi",
".biz": "text/xml",
".bot": "application/x-bot",
".c90": "application/x-c90",
".cat": "application/vnd.ms-pki.seccat",
".cdr": "application/x-cdr",
".cer": "application/x-x509-ca-cert",
".cgm": "application/x-cgm",
".class": "java/*",
".cmp": "application/x-cmp",
".cot": "application/x-cot",
".crt": "application/x-x509-ca-cert",
".css": "text/css",
".dbf": "application/x-dbf",
".dbx": "application/x-dbx",
".dcx": "application/x-dcx",
".dgn": "application/x-dgn",
".dll": "application/x-msdownload",
".dot": "application/msword",
".dtd": "text/xml",
".dwf": "application/x-dwf",
".dxb": "application/x-dxb",
".edn": "application/vnd.adobe.edn",
".eml": "message/rfc822",
".epi": "application/x-epi",
".eps": "application/postscript",
".exe": "application/x-msdownload",
".fdf": "application/vnd.fdf",
".fo": "text/xml",
".g4": "application/x-g4",
".tif": "image/tiff",
".gl2": "application/x-gl2",
".hgl": "application/x-hgl",
".hpg": "application/x-hpgl",
".hqx": "application/mac-binhex40",
".hta": "application/hta",
".htm": "text/html",
".htt": "text/webviewhtml",
".icb": "application/x-icb",
".ico": "application/x-ico",
".ig4": "application/x-g4",
".iii": "application/x-iphone",
".ins": "application/x-internet-signup",
".IVF": "video/x-ivf",
".jfif": "image/jpeg",
".jpe": "application/x-jpe",
".jpg": "image/jpeg",
".js": "application/x-javascript",
".la1": "audio/x-liquid-file",
".latex": "application/x-latex",
".lbm": "application/x-lbm",
".ls": "application/x-javascript",
".m1v": "video/x-mpeg",
".m3u": "audio/mpegurl",
".mac": "application/x-mac",
".math": "text/xml",
".mdb": "application/x-mdb",
".mht": "message/rfc822",
".mi": "application/x-mi",
".midi": "audio/mid",
".mml": "text/xml",
".mns": "audio/x-musicnet-stream",
".movie": "video/x-sgi-movie",
".mp2": "audio/mp2",
".mp3": "audio/mp3",
".mpa": "video/x-mpg",
".mpe": "video/x-mpeg",
".mpg": "video/mpg",
".mpp": "application/vnd.ms-project",
".mpt": "application/vnd.ms-project",
".mpv2": "video/mpeg",
".mpx": "application/vnd.ms-project",
".mxp": "application/x-mmxp",
".nrf": "application/x-nrf",
".odc": "text/x-ms-odc",
".p10": "application/pkcs10",
".p7b": "application/x-pkcs7-certificates",
".p7m": "application/pkcs7-mime",
".p7s": "application/pkcs7-signature",
".pci": "application/x-pci",
".pcx": "application/x-pcx",
".pdf": "application/pdf",
".pfx": "application/x-pkcs12",
".pic": "application/x-pic",
".pl": "application/x-perl",
".pls": "audio/scpls",
".png": "image/png",
".pot": "application/vnd.ms-powerpoint",
".ppm": "application/x-ppm",
".ppt": "application/vnd.ms-powerpoint",
".pr": "application/x-pr",
".prn": "application/x-prn",
".ps": "application/x-ps",
".ptn": "application/x-ptn",
".r3t": "text/vnd.rn-realtext3d",
".ram": "audio/x-pn-realaudio",
".rat": "application/rat-file",
".rec": "application/vnd.rn-recording",
".rgb": "application/x-rgb",
".rjt": "application/vnd.rn-realsystem-rjt",
".rle": "application/x-rle",
".rmf": "application/vnd.adobe.rmf",
".rmj": "application/vnd.rn-realsystem-rmj",
".rmp": "application/vnd.rn-rn_music_package",
".rmvb": "application/vnd.rn-realmedia-vbr",
".rnx": "application/vnd.rn-realplayer",
".rpm": "audio/x-pn-realaudio-plugin",
".rt": "text/vnd.rn-realtext",
".rtf": "application/x-rtf",
".sam": "application/x-sam",
".sdp": "application/sdp",
".sit": "application/x-stuffit",
".sld": "application/x-sld",
".smi": "application/smil",
".smk": "application/x-smk",
".sol": "text/plain",
".spc": "application/x-pkcs7-certificates",
".spp": "text/xml",
".sst": "application/vnd.ms-pki.certstore",
".stm": "text/html",
".svg": "text/xml",
".tdf": "application/x-tdf",
".tga": "application/x-tga",
".tif": "application/x-tif",
".tld": "text/xml",
".torrent": "application/x-bittorrent",
".txt": "text/plain",
".uls": "text/iuls",
".vda": "application/x-vda",
".vml": "text/xml",
".vsd": "application/vnd.visio",
".vss": "application/vnd.visio",
".vst": "application/x-vst",
".vsx": "application/vnd.visio",
".vxml": "text/xml",
".wax": "audio/x-ms-wax",
".wb2": "application/x-wb2",
".wbmp": "image/vnd.wap.wbmp",
".wk3": "application/x-wk3",
".wkq": "application/x-wkq",
".wm": "video/x-ms-wm",
".wmd": "application/x-ms-wmd",
".wml": "text/vnd.wap.wml",
".wmx": "video/x-ms-wmx",
".wp6": "application/x-wp6",
".wpg": "application/x-wpg",
".wq1": "application/x-wq1",
".wri": "application/x-wri",
".ws": "application/x-ws",
".wsc": "text/scriptlet",
".wvx": "video/x-ms-wvx",
".xdr": "text/xml",
".xfdf": "application/vnd.adobe.xfdf",
".xls": "application/vnd.ms-excel",
".xlw": "application/x-xlw",
".xpl": "audio/scpls",
".xql": "text/xml",
".xsd": "text/xml",
".xslt": "text/xml",
".x_b": "application/x-x_b",
".sisx": "application/vnd.symbian.install",
".ipa": "application/vnd.iphone",
".xap": "application/x-silverlight-app",
".zip": "application/x-zip-compressed",
}

443
out/production/uav-edu-mp/src/core/useDeviceCruise.js

@ -0,0 +1,443 @@
/**
* 设备按轨迹巡航
*/
import {computed, reactive, ref} from 'vue';
export const useDeviceCruise = () => {
const points = ref([]);
const isPlaying = ref(false);
const isPaused = ref(true);
const rotate = ref(0);
const elapsedMs = ref(0);
const timelyData = ref({});
const extMarkerOption = ref({});
const datumTime = computed(() => {
const [{ timestamp: startTs } = {}] = points.value || [];
return points.value.map(({ timestamp }) => timestamp - startTs);
});
const totalTime = computed(() => datumTime.value[datumTime.value.length - 1] || 0);
let initCallBack = () => {};
let moveCallBack = () => {};
function setInitCallBack(fn = () => {}) {
initCallBack = fn;
}
function setMoveCallBack(fn = () => {}) {
moveCallBack = fn;
}
function setCurrentTime(ms) {
if (ms < 0 || ms > totalTime.value) return;
elapsedMs.value = ms;
const index = datumTime.value.findIndex(item => item >= ms);
// this._that.currentIndex = index;
const point = points.value[index];
const { lng, lat, yaw } = point;
timelyData.value = { ...point };
moveCallBack({ lng, lat, yaw });
}
function initRenderDevice() {
const [point] = points.value || [];
if (!point) {
return;
}
const { lng, lat, yaw } = point;
timelyData.value = { ...point };
initCallBack({ lng, lat, yaw });
}
let timer
function autoRenderDevice() {
if (isPaused.value) return;
const index = datumTime.value.findIndex(item => item >= elapsedMs.value);
// this._that.currentIndex = index;
if (index === -1) {
isPlaying.value = false;
isPaused.value = true;
if (timer) {
clearTimeout(timer);
}
return;
}
const point = points.value[index];
timelyData.value = { ...point };
const { lng, lat, yaw } = point;
moveCallBack({ lng, lat, yaw });
elapsedMs.value += 100;
timer = setTimeout(autoRenderDevice, 100);
}
function play() {
isPlaying.value = true;
isPaused.value = false;
if (timer) {
clearTimeout(timer);
}
autoRenderDevice();
}
function pause() {
isPlaying.value = false;
isPaused.value = true;
if (timer) {
clearTimeout(timer);
}
}
return {
points,
isPlaying,
isPaused,
rotate,
elapsedMs,
timelyData,
datumTime,
totalTime,
setCurrentTime,
initRenderDevice,
autoRenderDevice,
play,
pause,
extMarkerOption,
setInitCallBack,
setMoveCallBack
}
}
// class DeviceCruise {
//
// _that = null;
//
// // 地图实例
// _map = null;
//
// // 单条轨迹数据
// _dataSource = {};
//
// // 巡航速率
// speedRate = 1;
//
// // 动画计时器
// _timer = null;
//
// // 上一帧时间点
// _lastFrameAt = 0;
//
// // 累计播放时长(毫秒数)
// elapsedMs = 0;
//
// // 每帧期望间隔(毫秒数,实际间隔取决于浏览器fps)
// _fpsInterval = 1000 / 50;
//
// // 上一帧时间戳
// _lastFrameTimestamp = 0;
//
// // 巡航到的时间点数据
// timelyData = {};
//
// isPlaying = false;
//
// isPaused = false;
//
// isStopped = true;
//
// // currentIndex = 0;
//
// // 是否准备完毕
// 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() {
// // console.log('this._that._datumTime', this._that._datumTime);
// return this._that._datumTime[this._that._datumTime.length - 1] || 0;
// }
//
// // 是否已经开始播放了
// get isStarted() {
// return this._that.isPlaying || this._that.isPaused;
// }
//
// constructor() {
// this._that = reactive(this);
// return this._that;
// }
//
// // 设置地图实例
// setMap(mapInstance) {
// this._that._map = mapInstance;
// }
//
// mapRotate = 0;
// // 载入轨迹数据(point中必须包含lng, lat, timestamp, yaw)
// loadTrack({ id, points, ...others }, rotate = 0) {
// // if (!this._that._that._map) {
// // throw new Error('请先设置地图实例');
// // }
// this._that.mapRotate = rotate;
// this._that._dataSource = {
// id,
// points: (points || []).map(item => {
// const [lng, lat] = GPS2GCJ([item.lng, item.lat]);
// return {
// ...item,
// lng,
// lat,
// yaw: +geo.radToDeg(item.yaw || 0).toFixed(1),
// };
// }),
// ...others,
// };
// this._that._reset();
// this._that._initDevice();
// }
//
// // 设置速率
// setSpeedRate(val) {
// this._that.speedRate = val;
// }
//
// _setting = false;
// // 设置当前巡航时间点
// setCurrentTime(ms) {
// if (ms < 0 || ms > this._that.totalTime) return;
// // if (this._that.isPlaying) {
// // this._that.isPlaying = false;
// // this._that.isPaused = true;
// // }
//
// // if (this._that._setting) return;
// // this._that._setting = true;
// this._that.elapsedMs = ms;
// const index = this._that._datumTime.findIndex(item => item >= ms);
// this._that.currentIndex = index;
// const point = this._that._points[index];
// const { lng, lat, yaw } = point;
// // this._that._map.removeMarkers({
// // markerIds: [markerId],
// // success: () => {
// // // this._that._renderDevice();
// // this._that._map.addMarkers({
// // markers: [{
// // ...this._that._markerOptins,
// // latitude: lat,
// // longitude: lng,
// // rotate: yaw + this._that.mapRotate,
// // }],
// // success: () => {
// // this._that._setting = false;
// // }
// // });
// // }
// // });
// this._that.markers.value = [{
// ...this._that._markerOptins,
// latitude: lat,
// longitude: lng,
// rotate: yaw + this._that.mapRotate,
// }];
// }
//
// // 获取指定毫秒处的数据值
// _getTimelyData(ms = 0) {
// // const genTimelyData = interpolate(this._that._datumTime, this._that._points);
// const index = this._that._datumTime.findIndex(item => item >= ms);
// // console.log('genTimelyData', genTimelyData(ms));
// // if (index === -1) {
// // return {};
// // }
//
// // return genTimelyData(ms);
// return this._that._points[index] || {};
// }
//
// _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;
// this._that.markers.value = [];
// }
//
// markers = ref([]);
// _initDevice() {
// const [point] = this._that._points;
// if (!point) {
// return;
// }
//
// const { lng, lat, yaw } = point;
// this._that.markers.value = [{
// ...this._that._markerOptins,
// latitude: lat,
// longitude: lng,
// rotate: yaw + this._that.mapRotate,
// }];
// // this._that._map.addMarkers({
// // markers: [{
// // ...this._that._markerOptins,
// // latitude: lat,
// // longitude: lng,
// // rotate: yaw + this._that.mapRotate,
// // }]
// // })
// }
//
// _clearDevice() {
// // 清除timelyData即可
// this._that.timelyData = {};
// // this._that._map.removeMarkers({
// // markerIds: [markerId]
// // });
// this._that.markers.value = [];
// }
//
// _renderDevice() {
// if (!this._that._points.length) return;
// // if (!this._that.isPlaying) return;
//
// // const lastData = this._that._getTimelyData(this._that.elapsedMs - 100 > 0 ? this._that.elapsedMs - 100 : 0);
// const nextData = this._that._getTimelyData(this._that.elapsedMs);
//
// // const { deep, breadth, seeding, flow } = nextData;
// this._that.timelyData = { ...nextData };
// const { lng, lat, yaw } = nextData;
//
// // this._that._map.translateMarker({
// // markerId,
// // destination: {
// // longitude: lng,
// // latitude: lat,
// // },
// // autoRotate: false,
// // duration: 1,
// // rotate: yaw + this._that.mapRotate,
// // moveWithRotate: true,
// // animationEnd: () => {
// // // this._that.timelyData = { ...nextData };
// // // this._that.currentIndex += 1;
// // // if (this._that.isPlaying) {
// // // this._that.elapsedMs += (duration * this._that.speedRate);
// // // if (this._that.elapsedMs >= this._that.totalTime) {
// // // this._that.handleStop();
// // // return;
// // // }
// // // this._that._renderDevice();
// // // }
// // }
// // });
//
// // this._that._map.removeMarkers({
// // markerIds: [markerId],
// // success: () => {
// // // this._that._renderDevice();
// // this._that._map.addMarkers({
// // markers: [{
// // ...this._that._markerOptins,
// // latitude: lat,
// // longitude: lng,
// // rotate: yaw + this._that.mapRotate,
// // }],
// // success: () => {
// // // this._that._setting = false;
// // }
// // });
// // }
// // });
// this._that.markers.value = [{
// ...this._that._markerOptins,
// latitude: lat,
// longitude: lng,
// rotate: yaw + this._that.mapRotate,
// }];
// }
//
// _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() {
// requestAnimationFrame(ms => {
// this._that._lastFrameAt = ms;
// });
// this._that.isPlaying = true;
// this._that.isPaused = false;
// this._that.isStopped = false;
// 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;
// }
// }
// export default new DeviceCruise();

466
out/production/uav-edu-mp/src/core/useDeviceCruise2.js

@ -0,0 +1,466 @@
/**
* 设备按轨迹巡航
*/
import {computed, reactive, ref} from 'vue';
// import { interpolate } from 'popmotion';
// import * as turf from '@turf/turf';
// import deviceIcon from '../assets/deviceIcon.png';
import deviceIcon from '../assets/droneImg.png';
// import { GPS2GCJ } from '../utils/helpers';
// import * as geo from "../utils/geo";
const markerId = 1e8;
const markerOption = {
id: markerId,
iconPath: deviceIcon,
width: 18,
height: 18,
anchor: { x: 0.5, y: 0.5 },
}
export const useDeviceCruise = () => {
const markers = ref([]);
const points = ref([]);
const isPlaying = ref(false);
const isPaused = ref(true);
const rotate = ref(0);
const elapsedMs = ref(0);
const timelyData = ref({});
const extMarkerOption = ref({});
const datumTime = computed(() => {
const [{ timestamp: startTs } = {}] = points.value || [];
return points.value.map(({ timestamp }) => timestamp - startTs);
});
const totalTime = computed(() => datumTime.value[datumTime.value.length - 1] || 0);
function setCurrentTime(ms) {
if (ms < 0 || ms > totalTime.value) return;
elapsedMs.value = ms;
const index = datumTime.value.findIndex(item => item >= ms);
// this._that.currentIndex = index;
const point = points.value[index];
const { lng, lat, yaw } = point;
timelyData.value = { ...point };
markers.value = [{
...markerOption,
...(extMarkerOption.value || {}),
latitude: lat,
longitude: lng,
rotate: yaw + rotate.value,
}];
}
function initRenderDevice() {
const [point] = points.value || [];
if (!point) {
return;
}
const { lng, lat, yaw } = point;
timelyData.value = { ...point };
// console.log(yaw, rotate.value, yaw + rotate.value);
markers.value = [{
...markerOption,
...(extMarkerOption.value || {}),
latitude: lat,
longitude: lng,
rotate: yaw + rotate.value,
}];
}
let timer
function autoRenderDevice() {
if (isPaused.value) return;
const index = datumTime.value.findIndex(item => item >= elapsedMs.value);
// this._that.currentIndex = index;
if (index === -1) {
isPlaying.value = false;
isPaused.value = true;
if (timer) {
clearTimeout(timer);
}
return;
}
const point = points.value[index];
timelyData.value = { ...point };
const { lng, lat, yaw } = point;
markers.value = [{
...markerOption,
...(extMarkerOption.value || {}),
latitude: lat,
longitude: lng,
rotate: yaw + rotate.value,
}];
// console.log('yaw', yaw);
elapsedMs.value += 100;
timer = setTimeout(autoRenderDevice, 100);
}
function play() {
isPlaying.value = true;
isPaused.value = false;
if (timer) {
clearTimeout(timer);
}
autoRenderDevice();
}
function pause() {
isPlaying.value = false;
isPaused.value = true;
if (timer) {
clearTimeout(timer);
}
}
return {
markers,
points,
isPlaying,
isPaused,
rotate,
elapsedMs,
timelyData,
datumTime,
totalTime,
setCurrentTime,
initRenderDevice,
autoRenderDevice,
play,
pause,
extMarkerOption,
}
}
// class DeviceCruise {
//
// _that = null;
//
// // 地图实例
// _map = null;
//
// // 单条轨迹数据
// _dataSource = {};
//
// // 巡航速率
// speedRate = 1;
//
// // 动画计时器
// _timer = null;
//
// // 上一帧时间点
// _lastFrameAt = 0;
//
// // 累计播放时长(毫秒数)
// elapsedMs = 0;
//
// // 每帧期望间隔(毫秒数,实际间隔取决于浏览器fps)
// _fpsInterval = 1000 / 50;
//
// // 上一帧时间戳
// _lastFrameTimestamp = 0;
//
// // 巡航到的时间点数据
// timelyData = {};
//
// isPlaying = false;
//
// isPaused = false;
//
// isStopped = true;
//
// // currentIndex = 0;
//
// // 是否准备完毕
// 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() {
// // console.log('this._that._datumTime', this._that._datumTime);
// return this._that._datumTime[this._that._datumTime.length - 1] || 0;
// }
//
// // 是否已经开始播放了
// get isStarted() {
// return this._that.isPlaying || this._that.isPaused;
// }
//
// constructor() {
// this._that = reactive(this);
// return this._that;
// }
//
// // 设置地图实例
// setMap(mapInstance) {
// this._that._map = mapInstance;
// }
//
// mapRotate = 0;
// // 载入轨迹数据(point中必须包含lng, lat, timestamp, yaw)
// loadTrack({ id, points, ...others }, rotate = 0) {
// // if (!this._that._that._map) {
// // throw new Error('请先设置地图实例');
// // }
// this._that.mapRotate = rotate;
// this._that._dataSource = {
// id,
// points: (points || []).map(item => {
// const [lng, lat] = GPS2GCJ([item.lng, item.lat]);
// return {
// ...item,
// lng,
// lat,
// yaw: +geo.radToDeg(item.yaw || 0).toFixed(1),
// };
// }),
// ...others,
// };
// this._that._reset();
// this._that._initDevice();
// }
//
// // 设置速率
// setSpeedRate(val) {
// this._that.speedRate = val;
// }
//
// _setting = false;
// // 设置当前巡航时间点
// setCurrentTime(ms) {
// if (ms < 0 || ms > this._that.totalTime) return;
// // if (this._that.isPlaying) {
// // this._that.isPlaying = false;
// // this._that.isPaused = true;
// // }
//
// // if (this._that._setting) return;
// // this._that._setting = true;
// this._that.elapsedMs = ms;
// const index = this._that._datumTime.findIndex(item => item >= ms);
// this._that.currentIndex = index;
// const point = this._that._points[index];
// const { lng, lat, yaw } = point;
// // this._that._map.removeMarkers({
// // markerIds: [markerId],
// // success: () => {
// // // this._that._renderDevice();
// // this._that._map.addMarkers({
// // markers: [{
// // ...this._that._markerOptins,
// // latitude: lat,
// // longitude: lng,
// // rotate: yaw + this._that.mapRotate,
// // }],
// // success: () => {
// // this._that._setting = false;
// // }
// // });
// // }
// // });
// this._that.markers.value = [{
// ...this._that._markerOptins,
// latitude: lat,
// longitude: lng,
// rotate: yaw + this._that.mapRotate,
// }];
// }
//
// // 获取指定毫秒处的数据值
// _getTimelyData(ms = 0) {
// // const genTimelyData = interpolate(this._that._datumTime, this._that._points);
// const index = this._that._datumTime.findIndex(item => item >= ms);
// // console.log('genTimelyData', genTimelyData(ms));
// // if (index === -1) {
// // return {};
// // }
//
// // return genTimelyData(ms);
// return this._that._points[index] || {};
// }
//
// _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;
// this._that.markers.value = [];
// }
//
// markers = ref([]);
// _initDevice() {
// const [point] = this._that._points;
// if (!point) {
// return;
// }
//
// const { lng, lat, yaw } = point;
// this._that.markers.value = [{
// ...this._that._markerOptins,
// latitude: lat,
// longitude: lng,
// rotate: yaw + this._that.mapRotate,
// }];
// // this._that._map.addMarkers({
// // markers: [{
// // ...this._that._markerOptins,
// // latitude: lat,
// // longitude: lng,
// // rotate: yaw + this._that.mapRotate,
// // }]
// // })
// }
//
// _clearDevice() {
// // 清除timelyData即可
// this._that.timelyData = {};
// // this._that._map.removeMarkers({
// // markerIds: [markerId]
// // });
// this._that.markers.value = [];
// }
//
// _renderDevice() {
// if (!this._that._points.length) return;
// // if (!this._that.isPlaying) return;
//
// // const lastData = this._that._getTimelyData(this._that.elapsedMs - 100 > 0 ? this._that.elapsedMs - 100 : 0);
// const nextData = this._that._getTimelyData(this._that.elapsedMs);
//
// // const { deep, breadth, seeding, flow } = nextData;
// this._that.timelyData = { ...nextData };
// const { lng, lat, yaw } = nextData;
//
// // this._that._map.translateMarker({
// // markerId,
// // destination: {
// // longitude: lng,
// // latitude: lat,
// // },
// // autoRotate: false,
// // duration: 1,
// // rotate: yaw + this._that.mapRotate,
// // moveWithRotate: true,
// // animationEnd: () => {
// // // this._that.timelyData = { ...nextData };
// // // this._that.currentIndex += 1;
// // // if (this._that.isPlaying) {
// // // this._that.elapsedMs += (duration * this._that.speedRate);
// // // if (this._that.elapsedMs >= this._that.totalTime) {
// // // this._that.handleStop();
// // // return;
// // // }
// // // this._that._renderDevice();
// // // }
// // }
// // });
//
// // this._that._map.removeMarkers({
// // markerIds: [markerId],
// // success: () => {
// // // this._that._renderDevice();
// // this._that._map.addMarkers({
// // markers: [{
// // ...this._that._markerOptins,
// // latitude: lat,
// // longitude: lng,
// // rotate: yaw + this._that.mapRotate,
// // }],
// // success: () => {
// // // this._that._setting = false;
// // }
// // });
// // }
// // });
// this._that.markers.value = [{
// ...this._that._markerOptins,
// latitude: lat,
// longitude: lng,
// rotate: yaw + this._that.mapRotate,
// }];
// }
//
// _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() {
// requestAnimationFrame(ms => {
// this._that._lastFrameAt = ms;
// });
// this._that.isPlaying = true;
// this._that.isPaused = false;
// this._that.isStopped = false;
// 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;
// }
// }
// export default new DeviceCruise();

49
out/production/uav-edu-mp/src/core/useDrone.js

@ -0,0 +1,49 @@
import { Image } from '@leafer-ui/miniapp';
import deviceIcon from "../assets/droneImg.png";
export const useDrone = () => {
let leafer;
let drone;
function setScene(Leafer) {
leafer = Leafer;
}
function createDrone({ x, y, rotation }) {
if (drone) {
drone.set({
x,
y,
rotation,
});
} else {
drone = new Image({
x,
y,
url: deviceIcon,
width: 200,
height: 200,
around: 'center', // 设置原点在中心
// draggable: true,
placeholderColor: 'rgba(120,120,120,0.2)', // 设置图片占位符的背景颜色 //
rotation,
zIndex: 3,
});
leafer.add(drone);
}
}
function moveDrone({ x, y, rotation }) {
drone.set({
x,
y,
rotation,
});
}
return {
createDrone,
setScene,
moveDrone
}
}

61
out/production/uav-edu-mp/src/core/useErrPoint.js

@ -0,0 +1,61 @@
import { Path, Text } from '@leafer-ui/miniapp';
export const useErrPoint = () => {
let leafer;
let bearing = 0;
let pathList = [];
let textList = [];
function remove() {
pathList.forEach(item => item.remove());
textList.forEach(item => item.remove());
}
function setScene(Leafer, Bearing) {
leafer = Leafer;
bearing = Bearing;
}
function createErrPoint(points = []) {
remove();
pathList = [];
textList = [];
points.forEach(((tmp, index) => {
const errPoint = new Path({
path: `P${tmp.x} ${tmp.y} 20`,
fill: "#ff0000",
strokeWidth: 2,
stroke: '#ffffff',
zIndex: 2,
})
const text = new Text({
x: tmp.x,
y: tmp.y,
fill: '#ff0000',
text: index + 1,
fontSize: 50,
around: 'center',
offsetX: -30,
offsetY: -60,
rotation: (bearing - 90),
stroke: {
type: 'solid',
color: '#ff0000'
},
zIndex: 2,
})
pathList.push(errPoint);
textList.push(text);
leafer.add(text);
leafer.add(errPoint);
}));
}
return {
createErrPoint,
setScene,
remove,
}
}

138
out/production/uav-edu-mp/src/core/useField.js

@ -0,0 +1,138 @@
import { Path, Text } from '@leafer-ui/miniapp';
import * as turf from "@turf/turf";
import { getXYUnit, GPS2XY } from '../utils/helpers';
export const useField = () => {
let leafer;
function setScene(Leafer) {
leafer = Leafer;
}
function createField(c1Config = {}, c2Config = {}, c3Config = {}) {
const bearing = turf.bearing(c1Config.LngLat, c2Config.LngLat); //
const origin = turf.midpoint(c1Config.LngLat, c2Config.LngLat);
const [originLng, originLat] = origin.geometry.coordinates;
const { xUnit, yUnit } = getXYUnit([originLng, originLat]);
const c1 = GPS2XY(c1Config.LngLat, [originLng, originLat], [xUnit, yUnit]);
const c1RMax = (c1Config.r + c1Config.rDiff) * 100;
const c1RMin = (c1Config.r - c1Config.rDiff) * 100;
const c1R = c1Config.r * 100;
const c2 = GPS2XY(c2Config.LngLat, [originLng, originLat], [xUnit, yUnit]);
const c2RMax = (c2Config.r + c2Config.rDiff) * 100;
const c2RMin = (c2Config.r - c2Config.rDiff) * 100;
const c2R = c2Config.r * 100;
const eightBg1 = new Path({
path: `P${c1.x} ${c1.y} ${c1RMin} M${c1.x + c1RMax} ${c1.y} P${c1.x} ${c1.y} ${c1RMax}`,
fill: '#ffffff5f',
windingRule: 'evenodd',
})
const eightBg1Center = new Path({
path: `P${c1.x} ${c1.y} 20`,
fill: '#FF0000'
})
const leftC = new Path({
path: `P${c1.x} ${c1.y} ${c1R}`,
stroke: '#FF0000',
strokeWidth: 4,
// windingRule: 'evenodd',
})
const eightBg2 = new Path({
path: `P${c2.x} ${c2.y} ${c2RMin} M${c2.x + c2RMax} ${c2.y} P${c2.x} ${c2.y} ${c2RMax}`,
fill: '#ffffff5f',
windingRule: 'evenodd',
})
const eightBg2Center = new Path({
path: `P${c2.x} ${c2.y} 20`,
fill: '#FF0000'
})
const rightC = new Path({
path: `P${c2.x} ${c2.y} ${c2R}`,
stroke: '#FF0000',
strokeWidth: 4,
// windingRule: 'evenodd',
})
leafer.add(eightBg1);
leafer.add(eightBg2);
leafer.add(eightBg1Center);
leafer.add(eightBg2Center);
leafer.add(leftC);
leafer.add(rightC);
const indexMap = [6, 0, 1, 3, 4, 5, 2];
const center1 = turf.point(c1Config.LngLat);
const center2 = turf.point(c2Config.LngLat);
const radius1 = c1Config.r;
const radius2 = c2Config.r;
// 生成基准交点
const basePoint1 = turf.destination(center1, radius1, bearing, { units: 'meters' }); // 圆1交点
const basePoint2 = turf.destination(center2, radius2, bearing + 180, { units: 'meters' }); // 圆2交点
// 生成旋转点集合
const generateRotatedPoints = (center, baseCoord) => {
const points = [];
for(let angle = 0; angle < 360; angle += 90) {
const rotatedCoord = turf.transformRotate(
turf.point(baseCoord),
angle,
{pivot: center}
).geometry.coordinates;
points.push(rotatedCoord);
}
return points.slice(1); // 排除原始点
};
// 生成所有标记点
const positions = [
...generateRotatedPoints(center1, basePoint1.geometry.coordinates),
...generateRotatedPoints(center2, basePoint2.geometry.coordinates),
turf.midpoint(center1, center2).geometry.coordinates // 连线中点
];
positions.forEach((coord, index) => {
// 为中点位置添加圆形背景
if (index === 6) {
const c3 = GPS2XY([coord[0], coord[1]], [originLng, originLat], [xUnit, yUnit]);
const c3R = c3Config.r * 100;
const midC = new Path({
path: `P${c3.x} ${c3.y} ${c3R}`,
stroke: '#FF0000',
strokeWidth: 4,
// windingRule: 'evenodd',
})
leafer.add(midC);
}
const tmp = GPS2XY([coord[0], coord[1]], [originLng, originLat], [xUnit, yUnit]);
const text = new Text({
x: tmp.x,
y: tmp.y,
fill: '#ffffff',
text: indexMap[index] + 1 + '',
fontSize: 80,
around: 'center',
// offsetX: -30,
// offsetY: -60,
rotation: (bearing - 90),
stroke: {
type: 'solid',
color: '#000000'
}
})
leafer.add(text);
});
}
return {
createField,
setScene,
}
}

39
out/production/uav-edu-mp/src/core/useLostPoint.js

@ -0,0 +1,39 @@
import { Path, Text } from '@leafer-ui/miniapp';
export const useLostPoint = () => {
let leafer;
function setScene(Leafer) {
leafer = Leafer;
}
function createLostPoint({ x, y }) {
const lBg = new Path({
path: `P${x} ${y} 40`,
fill: '#a60000',
stroke: '#ffffff',
strokeWidth: 4,
})
const l = new Text({
x,
y,
fill: '#ffffff',
text: 'L',
fontSize: 80,
around: 'center',
// stroke: {
// type: 'solid',
// color: '#000000'
// }
})
leafer.add(lBg);
leafer.add(l);
}
return {
createLostPoint,
setScene,
}
}

164
out/production/uav-edu-mp/src/core/useScene.js

@ -0,0 +1,164 @@
import Taro from '@tarojs/taro';
import {Leafer, Path, Rect, useCanvas, Text, Group, Ellipse} from '@leafer-ui/miniapp';
import '@leafer-in/view';
import '@leafer-in/viewport';
import { getXYUnit, GPS2XY } from "../utils/helpers";
import * as turf from "@turf/turf";
useCanvas('canvas', Taro) // 绑定平台全局变量
export const useScene = () => {
let leafer = undefined;
let oLng = 0;
let oLat = 0;
let xUnit = 0;
let yUnit = 0;
let bearing = 0;
function receiveEvent(event) {
if (leafer) {
leafer.receiveEvent(event) // 需手动接收、传递画布交互事件给leafer
}
}
function init() {
leafer = new Leafer({ view: 'leafer', type: 'viewport' });
}
function createXYAxis({ c1LngLat, c2LngLat } = {}) {
const [originLng, originLat] = turf.midpoint(c1LngLat, c2LngLat).geometry.coordinates;
const { xUnit: x, yUnit: y } = getXYUnit([originLng, originLat]);
xUnit = x;
yUnit = y;
oLng = originLng;
oLat = originLat;
//
bearing = turf.bearing(c1LngLat, c2LngLat);
}
let c1Config = {};
let c2Config = {};
let c3Config = {};
function loadFieldData(c1, c2, c3) {
c1Config = { ...c1 };
c2Config = { ...c2 };
c3Config = { ...c3 };
}
function createField(field) {
field.setScene(leafer);
field.createField(c1Config, c2Config, c3Config);
}
function fitView() {
if (bearing) {
leafer.rotateOf('center', -(bearing - 90));
}
leafer.zoom('fit');
}
let trackList = [];
function loadTrackData(data = []) {
trackList = data.map(item => GPS2XY([item.lng, item.lat], [oLng, oLat], [xUnit, yUnit]));
}
function createTrack(track) {
track.setScene(leafer);
track.createTrack(trackList);
}
let errList = [];
function loadErrData(data = []) {
errList = data.map((item) => {
return GPS2XY([item?.lng, item?.lat], [oLng, oLat], [xUnit, yUnit]);
});
}
function createErrPoint(errPoint) {
errPoint.setScene(leafer, bearing);
errPoint.createErrPoint(errList);
}
let droneData = {};
function loadDroneData(data = {}) {
const { x, y } = GPS2XY([data?.lng, data?.lat], [oLng, oLat], [xUnit, yUnit]);
droneData = { x, y, rotation: data.yaw };
}
function createDrone(drone) {
drone.setScene(leafer);
drone.createDrone(droneData);
}
function moveDrone(drone) {
drone.moveDrone(droneData);
}
let spinData = {};
function loadSpinData(data = {}) {
const { x, y } = GPS2XY([data?.lng, data?.lat], [oLng, oLat], [xUnit, yUnit]);
spinData = { x, y, r: data.spinR * 100 }
}
function createSpin(spin) {
spin.setScene(leafer);
spin.createSpin(spinData);
}
function removeSpin(spin) {
spin.remove();
}
function createTripXYAxis([originLng, originLat] = []) {
const { xUnit: x, yUnit: y } = getXYUnit([originLng, originLat]);
xUnit = x;
yUnit = y;
oLng = originLng;
oLat = originLat;
}
let tripData = {};
function loadTripData(data = {}) {
const { x, y } = GPS2XY([data?.lng, data?.lat], [oLng, oLat], [xUnit, yUnit]);
tripData = { x, y, oR: data.oR, iR: data.iR, r: data.r };
}
function createTripField(tripField) {
tripField.setScene(leafer);
tripField.createTripField(tripData);
}
let lostPointData = {};
function loadLostPointData(data = {}) {
lostPointData = GPS2XY([data?.lng, data?.lat], [oLng, oLat], [xUnit, yUnit]);
}
function createLostPoint(lostPoint) {
lostPoint.setScene(leafer);
lostPoint.createLostPoint(lostPointData);
}
return {
init,
createXYAxis,
loadFieldData,
createField,
receiveEvent,
fitView,
loadTrackData,
createTrack,
loadErrData,
createErrPoint,
loadDroneData,
createDrone,
moveDrone,
createSpin,
removeSpin,
loadSpinData,
createTripXYAxis,
loadTripData,
createTripField,
loadLostPointData,
createLostPoint,
}
}

34
out/production/uav-edu-mp/src/core/useSpin.js

@ -0,0 +1,34 @@
import { Path } from '@leafer-ui/miniapp';
export const useSpin = () => {
let leafer;
let spin;
function setScene(Leafer) {
leafer = Leafer;
}
function remove() {
if (spin) {
spin.remove();
}
}
function createSpin({ x, y, r }) {
const spinC = new Path({
path: `P${x} ${y} ${r}`,
stroke: '#ffd500',
strokeWidth: 4,
zIndex: 2
// windingRule: 'evenodd',
})
spin = spinC;
leafer.add(spinC)
}
return {
createSpin,
setScene,
remove,
}
}

37
out/production/uav-edu-mp/src/core/useTrack.js

@ -0,0 +1,37 @@
import { Path } from '@leafer-ui/miniapp';
export const useTrack = () => {
let leafer;
let line;
function remove() {
if (line) {
line.remove()
}
}
function setScene(Leafer) {
leafer = Leafer;
}
function createTrack(points = []) {
remove();
const pathData = points.map(((tmp, index) => {
if (index === 0) {
return [1, tmp.x, tmp.y];
}
return [2, tmp.x, tmp.y];
}));
line = new Path({
path: pathData.flat(),
stroke: '#25814f',
strokeWidth: 6,
zIndex: 1,
})
leafer.add(line);
}
return {
createTrack,
setScene,}
}

58
out/production/uav-edu-mp/src/core/useTripField.js

@ -0,0 +1,58 @@
import { Path, Text } from '@leafer-ui/miniapp';
export const useTripField = () => {
let leafer;
function setScene(Leafer) {
leafer = Leafer;
}
function createTripField(c = {}) {
const cRMax = c.oR * 100;
const cRMin = c.iR * 100;
const cR2 = c.r * 100;
const field = new Path({
path: `P${c.x} ${c.y} ${cRMin} M${c.x + cRMax} ${c.y} P${c.x} ${c.y} ${cRMax}`,
fill: '#ffffff5f',
windingRule: 'evenodd',
});
const saveC = new Path({
path: `P${c.x} ${c.y} ${cR2}`,
stroke: '#00e1ff',
strokeWidth: 4,
// windingRule: 'evenodd',
});
const hBg = new Path({
path: `P${c.x} ${c.y} 40`,
fill: '#18a600',
stroke: '#ffffff',
strokeWidth: 4,
})
const h = new Text({
x: c.x,
y: c.y,
fill: '#ffffff',
text: 'H',
fontSize: 80,
around: 'center',
// stroke: {
// type: 'solid',
// color: '#000000'
// }
})
leafer.add(field);
leafer.add(saveC);
leafer.add(hBg);
leafer.add(h);
}
return {
createTripField,
setScene,
}
}

17
out/production/uav-edu-mp/src/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no,address=no">
<meta name="apple-mobile-web-app-status-bar-style" content="white">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>uav-edu-mp</title>
<script><%= htmlWebpackPlugin.options.script % ></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

9
out/production/uav-edu-mp/src/pages/airfield/index.config.js

@ -0,0 +1,9 @@
export default definePageConfig({
navigationBarTitleText: '场地管理',
// disableSwipeBack: true,
// enablePullDownRefresh: true,
// backgroundTextStyle: 'dark',
// navigationStyle: 'custom',
// enablePullDownRefresh: false,
// disableScroll: false,
})

454
out/production/uav-edu-mp/src/pages/airfield/index.vue

@ -0,0 +1,454 @@
<script setup>
import {ref, computed, reactive, watch, onUnmounted} from 'vue';
import Taro from '@tarojs/taro';
import TabBar from '../../components/TabBar.vue'
import {useAirFieldsStore, useStandardStore} from "../../stores";
import { storeToRefs } from 'pinia';
// import { useSupervisionStore } from '../../stores';
Taro.hideHomeButton();
const { deleteAirField, getAirfieldList, getDroneList, airfieldBindClass, getClassList, getLicenseGradesList } = useAirFieldsStore();
const { droneList, airfieldList, classList, licenseGradesList } = storeToRefs(useAirFieldsStore());
const { getEnvList } = useStandardStore();
const { envList } = storeToRefs(useStandardStore());
function getDrone() {
getDroneList().catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
}
getDrone();
getClassList().catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
// getEnvList().catch(({ msg }) => {
// if (msg) openToast('warn', msg);
// })
// getLicenseGradesList().catch(({ msg }) => {
// if (msg) openToast('warn', msg);
// })
const droneColumns = computed(() => {
return droneList.value.map(item => ({
text: item.isOnline ? item.name : `${item.name}-离线`,
value: item.sn,
className: item.isOnline ? '' : 'noline',
isOnline: item.isOnline,
}))
});
const classColumns = computed(() => {
// return [
// {
// text: 'aaaaa',
// value: '5',
// },
// ]
return classList.value.map(item => ({
text: item.name,
value: item.id,
}));
})
// API
// const { getFieldList } = useSupervisionStore();
// const { fieldList } = storeToRefs(useSupervisionStore());
Taro.useDidShow(() => {
// 使API
// getFieldList().catch(err => {
// Taro.showToast({
// title: err?.description || '',
// icon: 'none'
// });
// });
});
// function onNavTo(id) {
// Taro.navigateTo({
// url: `/pages/airfieldMap/index?id=${id}`,
// });
// }
getList();
function getList() {
getAirfieldList().catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
}
function onCreateField() {
if (!droneColumns.value.length) {
openToast('warn', '您名下没有无人机');
return;
}
showDronePicker.value = true;
}
const isDeleted = ref(false);
const currentFieldId = ref();
function handleDeleteConfirm() {
if (!currentFieldId.value) return;
deleteAirField(currentFieldId.value).then(() => {
openToast('success', '成功删除');
getList();
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
}
function onHandleDelete(id) {
currentFieldId.value = id;
isDeleted.value = true;
}
let timer;
const showDronePicker = ref(false);
watch(showDronePicker, (nv) => {
if (nv) {
timer = setInterval(getDrone, 3000);
} else {
if (timer) clearInterval(timer);
}
})
onUnmounted(() => {
if (timer) clearInterval(timer);
})
function onConfirmDrone({ selectedOptions }) {
if (!selectedOptions[0].isOnline) {
openToast('warn', '当前设备离线中,请检查设备');
return;
}
Taro.navigateTo({
url: `/pages/airfieldMap/index?droneSn=${selectedOptions[0].value}`,
}).then(() => {
showDronePicker.value = false;
});
}
function onHandleBind(id) {
if (!classColumns.value.length) {
openToast('warn', '您名下没有班级');
return;
}
currentFieldId.value = id;
showClassPicker.value = true;
}
const showClassPicker = ref(false);
function onConfirmClass({ selectedOptions }) {
console.log(selectedOptions);
airfieldBindClass({
classId: selectedOptions[0].value,
airfieldId: currentFieldId.value
}).then(() => {
openToast('success', '成功指定');
showClassPicker.value = false;
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
}
const state = reactive({
msg: '错误提示',
type: 'warn',
show: false,
cover: true,
// title: '',
// bottom: '',
center: true,
});
function openToast(type = 'warn', msg = '错误提示') {
state.msg = msg;
state.type = type;
state.show = true;
}
function onHandleEditor() {
// showDronePicker.value = true;
}
</script>
<template>
<div :class="s.root">
<div class="field-list">
<div class="field-card" v-for="item in airfieldList" :key="item.id">
<div class="field-image">
<image mode="aspectFill" :src="item.imageUrl || 'http://gcs-edu.obs.cn-east-2.myhuaweicloud.com/tmp/2025/05/tmp_1746237402612144307.png'" alt="场地图片" />
<div class="status-badge" :class="{ 'failed': true }" v-if="item?.classDetailResp">
当前班级{{ item?.classDetailResp?.name || '-' }}
</div>
</div>
<div class="field-info">
<div class="field-name">
<span class="icon">📍</span>
{{ item.name }}
</div>
<div class="info-item">
<div class="item">
<span class="label">风速</span>
<span class="value">{{ item.envGradeName || '暂无' }}</span>
</div>
<div class="item">
<span class="label">执照</span>
<span class="value">{{ item.licenseGradeName || '暂无' }}</span>
</div>
</div>
<div class="info-item">
<span class="label">创建时间</span>
<span class="value">{{ item.createDate }}</span>
</div>
</div>
<div class="info-btn">
<div class="box delete" @click="onHandleDelete(item.id)">删除</div>
<div class="box detail" @click="onHandleEditor(item.id)" v-if="false">修改</div>
<div class="box bind" @click="onHandleBind(item.id)">指定班级</div>
</div>
</div>
<div class="create-field-card" @click="onCreateField">
<div>+</div>
</div>
</div>
</div>
<nut-toast :msg="state.msg" v-model:visible="state.show" :type="state.type" :cover="state.cover" :duration="2000" />
<nut-dialog
title="删除提示"
content="确定删除该场地吗?"
v-model:visible="isDeleted"
@ok="handleDeleteConfirm"
/>
<nut-popup v-model:visible="showDronePicker" position="bottom" :style="{ height: '40%' }">
<nut-picker :class="s.dronePicker" :columns="droneColumns" title="打点无人机" @confirm="onConfirmDrone" @cancel="showDronePicker = false" />
</nut-popup>
<nut-popup v-model:visible="showClassPicker" position="bottom" :style="{ height: '40%' }">
<nut-picker :columns="classColumns" title="选择班级" @confirm="onConfirmClass" @cancel="showClassPicker = false" />
</nut-popup>
<TabBar />
</template>
<style lang="less" module="s">
.dronePicker {
:global {
.noline {
color: #ae0000;
}
}
}
.root {
min-height: 100vh;
background-color: #f2fbff;
padding: 16px;
box-sizing: border-box;
:global {
.field-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
padding: 0;
.field-card {
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
}
.field-image {
width: 100%;
height: 160px;
overflow: hidden;
background-color: #f0f0f0;
position: relative;
//display: flex;
//align-items: center;
//justify-content: center;
image {
width: 100%;
height: 100%;
//height: unset;
}
img {
//width: 100%;
//height: 100%;
//min-height: 100%;
//object-fit: contain;
//object-fit: cover;
}
.status-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 12px;
font-size: 20px;
font-weight: 500;
color: #fff;
background-color: rgba(0, 0, 0, 0.6);
&.in-progress {
background-color: #1890ff;
}
&.passed {
background-color: #52c41a;
}
&.failed {
background-color: #ff4d4f;
}
}
}
.field-info {
padding: 12px;
font-size: 20px;
.student-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.student-name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.teacher-name {
font-size: 12px;
color: #666;
background-color: #f5f5f5;
padding: 2px 8px;
border-radius: 4px;
}
}
.field-name {
color: #333;
margin-bottom: 10px;
display: flex;
align-items: center;
background-color: #f8f8f8;
padding: 6px 8px;
border-radius: 6px;
.icon {
margin-right: 4px;
//font-size: 14px;
}
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 6px;
//font-size: 13px;
//line-height: 1.4;
&:last-child {
margin-bottom: 0;
}
.item {
flex: 1;
display: flex;
align-items: center;
}
.label {
color: #666;
margin-right: 8px;
flex-shrink: 0;
min-width: 70px;
}
.value {
color: #333;
flex: 1;
white-space: nowrap;
}
}
}
.info-btn {
display: flex;
font-size: 20px;
//align-items: center;
//justify-content: space-around;
.box {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
}
.detail {
color: white;
background-color: rgba(0, 101, 255, 0.81);
}
.bind {
color: white;
background-color: rgba(0, 188, 60, 0.83);
}
.delete {
color: white;
background-color: rgba(255, 1, 1, 0.91);
}
}
}
.create-field-card {
min-height: 300px;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #7f8c8d;
font-size: 40px;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
}
}
}
}
}
</style>

101
out/production/uav-edu-mp/src/pages/airfieldMap/BottomSide.vue

@ -0,0 +1,101 @@
<script setup>
import { computed, ref } from 'vue';
import { IconFont } from "@nutui/icons-vue-taro";
import { falsyTo } from '../../utils/helpers';
// import deviceCruise from '../../core/useDeviceCruise';
// import deviceIcon from '../../../assets/deviceIcon.png'
const isVisible = ref(true);
defineProps({
info: {
type: Object,
default: () => ({}),
}
})
</script>
<template>
<div :class="s.root" :catch-move="true">
<!-- <canvas class="canvas-bg" type="2d" />-->
<div class="item">
<IconFont font-class-name="iconfont" class-prefix="icon" name="uav4" :style="{ color: info?.droneOnLine ? 'snow' : 'red', 'font-size': '24px' }" />
<div class="value" v-if="info?.connectLoading">连接中...</div>
<div class="value" v-else :style="{ color: info?.droneOnLine ? 'snow' : 'red' }">{{ info?.droneOnLine ? '已连接' : '飞机失联..' }}</div>
</div>
<div class="item">
<IconFont font-class-name="iconfont" class-prefix="icon" :name="info?.sysStatus === '已解锁' ? 'unlock' : 'lock'" />
<div class="value">{{ info?.sysStatus }}</div>
</div>
<div class="item">
<IconFont font-class-name="iconfont" class-prefix="icon" name="battery" />
<div class="value">{{ falsyTo(info?.voltage, '-') }} v</div>
</div>
<div class="item">
<IconFont font-class-name="iconfont" class-prefix="icon" name="measure" :style="{ transform: 'rotate(90deg)' }" />
<div class="value">{{ falsyTo(info?.height, '-') }} m</div>
</div>
<div class="item">
<IconFont font-class-name="iconfont" class-prefix="icon" name="compass" :style="{ transform: 'rotate(45deg)' }"/>
<div class="value">{{ (((info?.yaw ?? 0) + 360) % 360).toFixed(2) }}°</div>
</div>
<div class="item">
<IconFont font-class-name="iconfont" class-prefix="icon" name="satellite" />
<div class="value">{{ falsyTo(info?.satellite, '-') }} {{ falsyTo(info?.fixTypeLabel, '-')}}</div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
right: 200px;
left: 0;
//top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
color: snow;
font-size: 14px;
font-weight: bold;
//width: 100px;
//min-width: 100px;
z-index: 2;
display: flex;
align-items: center;
//justify-content: space-around;
gap: 10px;
padding: 10px;
//color: black;
:global {
//.canvas-bg {
// width: 100%;
// height: 100%;
// position: absolute;
//}
.item {
flex: 1;
width: fit-content;
white-space: nowrap;
display: flex;
align-items: center;
text-align: left;
font-family: monospace;
//color: white;
.iconfont {
width: 10px;
height: 10px;
font-size: 16px;
//color: #4CAF50;
}
.value {
margin-left: 8px;
font-size: 12px;
}
}
}
}
</style>

222
out/production/uav-edu-mp/src/pages/airfieldMap/LeftSide.vue

@ -0,0 +1,222 @@
<script setup>
import { computed, ref } from 'vue';
import { Left, Right } from '@nutui/icons-vue-taro';
// import { toFixed } from '@/utils/helpers.js';
import { toFixed } from '../../utils/helpers';
// import deviceCruise from '../../../core/deviceCruise';
import { IconFont } from "@nutui/icons-vue-taro";
const isVisible = ref(true);
// const info = computed(() => ({}));
defineProps({
info: {
type: Object,
default: () => ({}),
}
})
function onBack() {
console.log('aaaaaa');
}
</script>
<template>
<div :class="s.root" :catch-move="true">
<!-- <canvas class="canvas-bg" type="2d" />-->
<div class="box">
<div class="countdown">
<div class="title">倒计时</div>
<div class="value">{{ toFixed(info.height || 177, 1) }}</div>
</div>
</div>
<div class="box box2">
<div class="data-item">
<div class="text">切线速度</div>
<div class="icon-wrapper" v-if="false" >
<IconFont font-class-name="iconfont" class-prefix="icon" name="yibiaopan" />
</div>
<div class="value-wrapper">{{ toFixed(info?.speed || 0, 2) }}</div>
</div>
<div class="data-item">
<div class="text">航向偏差</div>
<div class="icon-wrapper" v-if="false" >
<IconFont font-class-name="iconfont" class-prefix="icon" name="hangxiang" />
</div>
<div class="value-wrapper">{{ toFixed(info?.angle || 0, 2) }}</div>
</div>
<div class="data-item">
<div class="text">高度偏差</div>
<div class="icon-wrapper" v-if="false" >
<IconFont font-class-name="iconfont" class-prefix="icon" name="gaodu" />
</div>
<div class="value-wrapper">{{ toFixed(info?.vertical || 0, 2) }}</div>
</div>
<div class="data-item">
<div class="text">水平偏差</div>
<div class="icon-wrapper" v-if="false" >
<IconFont font-class-name="iconfont" class-prefix="icon" name="width" />
</div>
<div class="value-wrapper">{{ toFixed(info?.horizontal || 0, 2) }}</div>
</div>
<div class="data-item">
<div class="text">角速度</div>
<div class="icon-wrapper" v-if="false" >
<IconFont font-class-name="iconfont" class-prefix="icon" name="jiaodu" />
</div>
<div class="value-wrapper">{{ toFixed(info?.angleSpeed || 0, 1) }}</div>
</div>
</div>
<div class="box box3">
<div class="data-item">
<div class="icon-wrapper">
<IconFont font-class-name="iconfont" class-prefix="icon" name="yibiaopan" />
</div>
<div class="value-wrapper">{{ toFixed(info?.yaw || 0, 1) }}</div>
</div>
</div>
<div class="box box4" @click="onBack">
<span>返回</span>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
left: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
// padding: 8px;
color: white;
font-size: 11px;
width: 100px;
z-index: 2;
display: flex;
flex-direction: column;
:global {
.canvas-bg {
width: 100%;
height: 80%;
position: absolute;
//top: 0;
//right: 0;
//left: 0;
//bottom: 20px;
}
.nav {
position: absolute;
right: -20px;
top: 6px;
.toggle-btn {
width: 20px;
height: 20px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 0 4px 4px 0;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
}
&.is-active {
background-color: rgba(0, 0, 0, 0.9);
}
}
}
.box {
display: flex;
flex-direction: column;
gap: 2px;
font-family: monospace;
color: #7FFF00;
border-bottom: 1px solid rgb(255, 255, 255);
// padding: 4px 8px;
.countdown {
display: flex;
align-items: center;
justify-content: center;
//justify-content: ;
.title {
}
.value {
font-size: 24px;
font-weight: bold;
text-align: center;
padding: 5px;
}
}
.data-item {
display: flex;
align-items: center;
justify-content: center;
// gap: 12px;
// padding: 5px;
.text {
font-size: 9px;
white-space: nowrap;
overflow: hidden;
}
.icon-wrapper {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: rgba(127, 255, 0, 0.1);
border-radius: 4px;
}
.value-wrapper {
flex: 1;
font-size: 18px;
font-weight: 500;
text-align: left;
padding-left: 8px;
}
}
}
.box2 {
padding: 4px 8px;
}
.box3 {
flex: 1;
color: red;
padding: 4px 8px;
.icon-wrapper {
background: rgba(255, 0, 0, 0.1) !important;
}
}
.box4 {
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
//height: 40px;
color: white;
//text-align: center;
border-bottom: none;
}
}
}
</style>

124
out/production/uav-edu-mp/src/pages/airfieldMap/ResultModal.vue

@ -0,0 +1,124 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
show: {
type: Boolean,
default: false
},
name: {
type: String,
default: '张三'
},
uavId: {
type: String,
default: 'UAV-001'
},
isPassed: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:show']);
const handleClose = () => {
emit('update:show', false);
};
</script>
<template>
<view v-show="show" :class="s.root">
<view class="modal-content">
<view class="close-btn" @tap="handleClose">×</view>
<view class="result-status" :class="{ 'pass': isPassed }">
{{ isPassed ? '通过' : '未通过' }}
</view>
<view class="info-item">
<text class="label">姓名</text>
<text class="value">{{ name }}</text>
</view>
<view class="info-item">
<text class="label">飞机编号</text>
<text class="value">{{ uavId }}</text>
</view>
</view>
</view>
</template>
<style lang="less" module="s">
.root {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
:global{
.modal-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 20px;
border-radius: 12px;
min-width: 280px;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.close-btn {
position: absolute;
top: 10px;
right: 10px;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
font-size: 20px;
color: #999;
cursor: pointer;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background-color: #f5f5f5;
color: #666;
}
}
.result-status {
font-size: 20px;
font-weight: bold;
text-align: center;
margin-bottom: 16px;
color: #ff4d4f;
&.pass {
color: #52c41a;
}
}
.info-item {
margin: 8px 0;
display: flex;
align-items: center;
.label {
color: #838383;
margin-right: 8px;
font-size: 14px;
}
.value {
color: #ffffff;
font-weight: 500;
font-size: 14px;
}
}
}
}
}
</style>

342
out/production/uav-edu-mp/src/pages/airfieldMap/RightSide.vue

@ -0,0 +1,342 @@
<script setup>
import {computed, reactive, ref} from 'vue';
import { storeToRefs } from 'pinia';
import {useAirFieldsStore, useAuthStore, useStandardStore} from "../../stores";
import Taro from "@tarojs/taro";
import { Uploader } from '@nutui/icons-vue-taro';
import FormData from '../../core/FormData';
// import {useDroneMarker} from "./useDroneMarker";
const { uploadFile } = useAuthStore();
const { getEnvList } = useStandardStore();
const { envList } = storeToRefs(useStandardStore());
const { getLicenseGradesList, createAirfield, resetFormData } = useAirFieldsStore();
const { licenseGradesList, formData } = storeToRefs(useAirFieldsStore());
getEnvList().then(() => {
if (envList.value.length) {
formData.value.envGradeId = envList.value[0]?.id;
}
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
const envGradeOptions = computed(() => {
return envList.value.map(item => ({
text: item.gradeName,
value: item.id,
}));
});
getLicenseGradesList().then(() => {
if (licenseGradesList.value.length) {
formData.value.licenseGradeId = licenseGradesList.value[0]?.id;
}
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
const licenseGradesOptions = computed(() => {
return licenseGradesList.value.map(item => ({
text: item.name,
value: item.id,
}));
});
const emit = defineEmits(['create'])
function createCircle(index) {
emit('create', index);
}
const loading = ref(false);
function onCreateField() {
if (!formData.value.name) {
openToast('warn', '请填写场地名称');
return;
}
if (!formData.value.circle1Lat || !formData.value.circle1Lng) {
openToast('warn', '请先给圆1打点');
return;
}
if (!formData.value.circle2Lat || !formData.value.circle2Lng) {
openToast('warn', '请先给圆2打点');
return;
}
if (!formData.value.envGradeId) {
openToast('warn', '请选择风速等级');
return;
}
if (!formData.value.licenseGradeId) {
openToast('warn', '请选择执照等级');
return;
}
loading.value = true;
if (formData.value.files.length) {
const form = new FormData();
// console.log('formData.files', formData.value.files);
formData.value.files.forEach(item => {
form.appendFile('file', item.url);
});
form.append('type', 'airfield');
const fileData = form.getData();
uploadFile(fileData.buffer, { header: { 'content-type': `${fileData.contentType}` } }).then(({ data }) => {
console.log('airfield', data.fileUrl);
formData.value.imageUrl = data.fileUrl || '';
form.clearCacheData();
createAirfield(formData.value).then(() => {
openToast('success', '成功创建场地');
onBack();
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
}).finally(() => {
loading.value = false;
})
}).catch(({ msg }) => {
if (msg) Taro.showToast({ title: msg, icon: 'error' });
});
return;
}
createAirfield(formData.value).then(() => {
openToast('success', '成功创建场地');
onBack();
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
}).finally(() => {
loading.value = false;
})
}
function onBack() {
Taro.navigateBack().then(() => {
resetFormData();
})
}
const state = reactive({
msg: '错误提示',
type: 'warn',
show: false,
cover: true,
// title: '',
// bottom: '',
center: true,
});
function openToast(type = 'warn', msg = '错误提示') {
state.msg = msg;
state.type = type;
state.show = true;
}
function beforeUpload({ fileList = [] } = {}) {
// fileList.map(item => {
// formData.value.files.push({
// type: item.fileType,
// url: item.tempFilePath,
// status: 'success',
// })
// })
const tmp = [{...(fileList[fileList.length - 1] || {})}];
formData.value.files = tmp.map(item => {
return {
type: item.type,
url: item.url,
status: 'success',
}
})
// console.log(e);
// return false;
}
const showPreview = ref(false);
const imgData = computed(() => {
return formData.value.files.map(item => ({ src: item.url }));
})
</script>
<template>
<div :class="s.root" :catch-move="true">
<div class="title">创建场地</div>
<!-- <canvas class="canvas-bg" type="2d" />-->
<div class="row">
<div class="label">场地名称</div>
<div class="ipt">
<nut-input v-model="formData.name" placeholder="请输入名称" />
</div>
</div>
<div class="row">
<div class="label">风速等级</div>
<div class="ipt">
<nut-radio-group v-model="formData.envGradeId">
<nut-radio v-for="item in envGradeOptions" :label="item.value" :key="item.value" shape="button">{{ item.text }}</nut-radio>
</nut-radio-group>
</div>
</div>
<div class="row">
<div class="label">执照等级</div>
<div class="ipt">
<nut-radio-group v-model="formData.licenseGradeId">
<nut-radio v-for="item in licenseGradesOptions" :label="item.value" :key="item.value" shape="button">{{ item.text }}</nut-radio>
</nut-radio-group>
</div>
</div>
<div class="row">
<div class="label">左侧圆心</div>
<div class="ipt">
<nut-button type="info" @click="createCircle(1)">打点</nut-button>
</div>
</div>
<div class="row">
<div class="label">右侧圆心</div>
<div class="ipt">
<nut-button type="info" @click="createCircle(2)">打点</nut-button>
</div>
</div>
<div class="row">
<div class="label">场地图片</div>
<div class="ipt">
<div class="uploader-box">
<nut-uploader :auto-upload="false" v-model:file-list="formData.files" @change="beforeUpload" :maximum="2" :media-type="['image']">
<nut-button type="success" size="small">选择图片</nut-button>
</nut-uploader>
<nut-button v-if="imgData.length" title="展示图片预览" size="small" @click="showPreview = true">预览</nut-button>
</div>
</div>
</div>
<div class="submit">
<nut-button type="primary" shape="square" :loading="loading" @click="onCreateField">创建</nut-button>
<nut-button type="info" shape="square" @click="onBack">返回</nut-button>
</div>
</div>
<nut-image-preview :show-index="false" :show="showPreview" :images="imgData" @close="showPreview = false" />
<nut-toast :msg="state.msg" v-model:visible="state.show" :type="state.type" :cover="state.cover" :duration="2000" />
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 11px;
width: 200px;
//min-width: 100px;
z-index: 2;
display: flex;
flex-direction: column;
gap: 10px;
:global {
.title {
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid snow;
padding: 6px 0;
font-size: 16px;
}
.row {
margin-top: 6px;
display: flex;
align-items: center;
& + .row {
}
.label {
width: 60px;
text-align: right;
}
.ipt {
.nut-input {
padding: 0;
font-size: inherit;
background-color: transparent;
color: white;
border-bottom: 1px solid rgba(245, 245, 245, 0.44);
.input-text {
font-size: inherit;
}
}
.nut-radio-group {
display: flex;
gap: 5px;
.nut-radio {
margin: 0;
.nut-radio__button {
padding: 2px 8px;
font-size: 9px;
}
}
}
.nut-button {
padding: 2px 6px;
font-size: 11px;
max-height: 20px;
}
.uploader-box {
display: flex;
align-items: center;
gap: 8px;
.picture {
display: none;
}
}
}
}
.submit {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
//margin-top: 40px;
display: flex;
flex-direction: column;
padding: 0 10px;
.nut-button {
padding: 2px 6px;
font-size: 14px;
max-height: 20px;
+ .nut-button {
margin-top: 10px;
}
}
}
//.bottom {
// border-top: 1px solid snow;
// display: flex;
// //ali
//
// .nut-button {
// padding: 2px 6px;
// font-size: 11px;
// max-height: 20px;
// }
//}
}
}
</style>

118
out/production/uav-edu-mp/src/pages/airfieldMap/geo.js

@ -0,0 +1,118 @@
/**
* 地理空间函数库
*/
import * as math from 'mathjs';
import * as turf from '@turf/turf';
/**
* 将两个点连成一个向量并确保落在14象限
* @param point1
* @param point2
* @returns {Vector}
*/
export function pointToVector(point1, point2) {
const vec = math.subtract(point2, point1);
return vec[0] < 0 ? math.multiply(-1, vec) : vec;
}
/**
* 弧度2角度
* @param radians
* @returns {number}
*/
export function radToDeg(radians) {
return math.multiply(radians, math.divide(180, math.pi));
}
/**
* 计算二维向量与x轴的夹角弧度
* @param {number[]} vector - 二维向量 [x, y]
* @returns {number} 夹角的弧度值
*/
export function angleWithXAxis(vector) {
const [x] = vector;
const magnitude = math.norm(vector);
if (magnitude === 0) {
throw new Error('零向量的夹角未定义');
}
const cosTheta = x / magnitude;
// 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
const safeCosTheta = Math.max(-1, Math.min(1, cosTheta));
return math.acos(safeCosTheta);
}
/**
* 计算二维向量与 y 轴的夹角弧度
* @param {number[]} vector - 二维向量 [x, y]
* @returns {number} 夹角的弧度值
*/
export function angleWithYAxis(vector) {
const [, y] = vector;
const magnitude = math.norm(vector); // 计算向量的模长 ||v||
if (magnitude === 0) {
throw new Error('零向量的夹角未定义');
}
const cosTheta = y / magnitude;
// 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
const safeCosTheta = math.max(-1, math.min(1, cosTheta));
// 计算夹角 theta
return math.acos(safeCosTheta);
}
/**
* 计算两个二维向量之间的夹角弧度
* @param {number[]} vectorA - 第一个二维向量 [x1, y1]
* @param {number[]} vectorB - 第二个二维向量 [x2, y2]
* @returns {number} 夹角的弧度值
*/
export function angleBetweenVectors(vectorA, vectorB) {
const dotProduct = math.dot(vectorA, vectorB);
const magnitudeA = math.norm(vectorA);
const magnitudeB = math.norm(vectorB);
if (magnitudeA === 0 || magnitudeB === 0) {
throw new Error('其中一个向量是零向量,夹角未定义');
}
const cosTheta = dotProduct / (magnitudeA * magnitudeB);
// 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
const safeCosTheta = Math.max(-1, Math.min(1, cosTheta));
return math.acos(safeCosTheta);
}
/**
* 计算两个坐标点的中点
* @param point1
* @param point2
* @returns {number[]}
*/
export function midPoint(point1, point2) {
const [x1, y1] = point1;
const [x2, y2] = point2;
const x = (x1 + x2) / 2;
const y = (y1 + y2) / 2;
return [+x.toFixed(8), +y.toFixed(8)];
}
/**
* 生成圆形路径
* @param center 圆心[lng, lat]
* @param radius 半径单位
* @param steps 分段数
* @returns {any}
*/
export function genCirclePath(center, radius, steps = 64) {
const geojson = turf.circle(center, radius / 1000, {
steps,
units: 'kilometers',
});
const [coords] = turf.getCoords(geojson);
return coords;
}

5
out/production/uav-edu-mp/src/pages/airfieldMap/index.config.js

@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '场地',
navigationStyle: 'custom',
pageOrientation: 'landscape'
})

452
out/production/uav-edu-mp/src/pages/airfieldMap/index.vue

@ -0,0 +1,452 @@
<script setup>
import {onMounted, ref, onUnmounted, reactive, watch, computed} from "vue";
import * as Taro from "@tarojs/taro";
import * as turf from '@turf/turf';
import LeftSide from './LeftSide.vue';
import RightSide from './RightSide.vue';
import BottomSide from './BottomSide.vue';
import { creatEightShaped } from '../flightMap/utils';
// import ResultModal from './ResultModal.vue';
import { useAirFieldsStore, useStandardStore, useSupervisionStore } from "../../stores";
import { useDroneMarker } from "./useDroneMarker";
import { storeToRefs } from 'pinia';
import { GPS2GCJ } from '../../utils/helpers';
const { markers, rotate, circles, createCircle, showEight, polygons, setCallBack, polyline, distanceText } = useDroneMarker();
const { getExamList, getEnvList } = useStandardStore()
const { wsDroneData } = useSupervisionStore();
const { position, attitude, battery, gps, tip, deviation, connectLoading, connectDrone, droneOnLine, sysStatus } = storeToRefs(useSupervisionStore());
const { getAirFieldsOfStudent } = useAirFieldsStore();
const { formData } = storeToRefs(useAirFieldsStore());
const { examList, envList } = storeToRefs(useStandardStore());
const envItem = computed(() => {
return envList.value.find((item) => item.id === formData.value.envGradeId) || {};
});
const standardData = computed(() => {
return examList.value.find((item) => item.name === '8字圆圈半径' && item.licenseLevelId === formData.value.licenseGradeId && item.envGrade === envItem.value.gradeName) || {};
});
const standardDiffData = computed(() => {
return examList.value.find((item) => item.name === '8字水平偏差阈值' && item.licenseLevelId === formData.value.licenseGradeId && item.envGrade === envItem.value.gradeName) || {};
});
const point3standardData = computed(() => {
return examList.value.find((item) => item.name === '点3中心筒范围-内' && item.licenseLevelId === formData.value.licenseGradeId && item.envGrade === envItem.value.gradeName) || {};
});
getExamList().catch(({ msg }) => {
if (msg) openToast('warn', msg);
});
const { params } = Taro.useRouter();
console.log('params', params);
//
let mapContext = null;
const show = ref(true);
//
onMounted(() => {
mapContext = Taro.createMapContext('map');
show.value = false;
connect();
// init();
// showEight();
});
onUnmounted(() => {
// eventBus.off('show-teaching-track-replay')
if (ws) {
confirmClose.value = true;
ws.close();
ws = null;
}
})
//
// const polygons = ref([]);
//
// const circles = ref([]);
//
const markerPoints = ref([]);
//
const bearing = ref(0);
let ws;
const confirmClose = ref(false);
function connect() {
// if (!params?.droneSn) {
// openToast('warn', '');
// return;
// }
wsDroneData(params?.droneSn).then(wsTask => {
ws = wsTask;
wsTask.onMessage( () => {
if (!center.value.lat || !center.value.lng) {
const { lng: Lng, lat: Lat } = position.value || {};
const [lng, lat] = GPS2GCJ([Lng, Lat]);
center.value.lat = lat;
center.value.lng = lng;
}
// if (firstMarker.value && position.value?.lng) {
// initDevice({ ...position.value, ...attitude.value }, bearing.value);
// firstMarker.value = false;
// } else {
// moveDevice({ ...position.value, ...attitude.value }, bearing.value)
// }
});
wsTask.onError(() => {
// clearDevice()
// setTimeout(() => {
// sum += 1;
// connect();
// }, 3000);
ws.close()
});
wsTask.onClose(() => {
// initDevice();
if (confirmClose.value) return;
// ws.close();
setTimeout(() => {
// sum += 1;
connect();
}, 3000);
})
});
}
const enableRotate = ref(false);
// function init() {
// //
// Promise.all([
// getExamList(),
// getEnvList(),
// // getAirFieldsDetail('9' || '10'),
// // getAirFieldsOfStudent(),
// ]).then(([examlist, envlist, airFieldsData]) => {
// console.log('air', airFieldsData);
// // const airfield = airFieldsData?.data || {};
// const { records = [] } = examlist || {}
// const envItem = (envlist || []).find((item) => item.id === airfield.envGradeId) || {}
// const standardData = records.find(
// (item) =>
// item.name === '8' &&
// item.licenseLevelId === airfield.licenseGradeId &&
// item.envGrade === envItem.gradeName,
// )
// const standardDiffData = records.find(
// (item) =>
// item.name === '8' &&
// item.licenseLevelId === airfield.licenseGradeId &&
// item.envGrade === envItem.gradeName,
// )
// const point3standardData = records.find(
// (item) =>
// item.name === '3-' &&
// item.licenseLevelId === airfield.licenseGradeId &&
// item.envGrade === envItem.gradeName,
// );
//
// const center1 = GPS2GCJ([airfield.circle1Lng, airfield.circle1Lat]);
// const center2 = GPS2GCJ([airfield.circle2Lng, airfield.circle2Lat]);
//
// const { polygons: shapePolygons, circles: shapeCircles, markers: shapeMarkers } = creatEightShaped(
// {
// center: center1,
// radius: standardData.value,
// radiusDiff: standardDiffData.value,
// centerWidth: 0.1,
// },
// {
// center: center2,
// radius: standardData.value,
// radiusDiff: standardDiffData.value,
// centerWidth: 0.1,
// },
// [6, 0, 1, 3, 4, 5, 2],
// { radius: point3standardData.value },
// );
//
// // //
// // polygons.value = shapePolygons;
// // setTimeout(() => {
// // circles.value = shapeCircles;
// // markerPoints.value = shapeMarkers;
// // }, 500);
//
// //
// mapContext.includePoints({
// points: [
// { latitude: center1[1], longitude: center1[0] },
// { latitude: center2[1], longitude: center2[0] }
// ],
// // padding: [130, 50, 130, 10],
// success: (res) => {
// setTimeout(() => {
// //
// const angle = turf.bearing(center1, center2);
// // 使
// rotate.value = 90 - angle;
// bearing.value = 90 - angle;
// connect();
// }, 500);
// // console.log('', res);
// },
// fail: (err) => {
// enableRotate.value = true;
// console.error('', err);
// }
// });
// }).catch(({ msg }) => {
// if (msg) openToast('warn', msg);
// })
// }
const center = ref({
lat: 0,
lng: 0,
})
setCallBack((index) => {
const { [index]: label } = { 1: '左圆心', 2: '右圆心' }
openToast('success', `成功打点${label}`);
})
const state = reactive({
msg: '错误提示',
type: 'warn',
show: false,
cover: true,
// title: '',
// bottom: '',
center: true,
});
function openToast(type = 'warn', msg = '错误提示') {
state.msg = msg;
state.type = type;
state.show = true;
}
function onHandleCreateCircle(index) {
// if (gps.value.fixType !== 'GPS_FIX_TYPE_RTK_FIXED' && gps.value.fixType !== 'GPS_FIX_TYPE_RTK_FLOAT') {
if (gps.value.fixType !== 'GPS_FIX_TYPE_RTK_FIXED') {
openToast('warn', '请确保无人机处于RTK模式下');
return;
}
// let next;
if (index === 2 && formData.value.circle1Lat) {
const { lng: Lng, lat: Lat } = position.value || {};
const [lng, lat] = GPS2GCJ([Lng, Lat]);
// const center1 = [position.value.lng, position.value.lat];
// const center2 = GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]);
const center1 = turf.point([lng, lat]);
const center2 = turf.point(GPS2GCJ([formData.value.circle1Lng, formData.value.circle1Lat]));
const dis = turf.distance(center1, center2, { units: 'meters' }).toFixed(1);
const tmp = (standardData.value?.value || 0) * 2;
console.log('ada', dis, tmp);
if (Math.abs(tmp - dis) > 0.5) {
openToast('warn', '请确保误差处于50厘米内');
return;
}
}
if (index === 1 && formData.value.circle2Lat) {
const { lng: Lng, lat: Lat } = position.value || {};
const [lng, lat] = GPS2GCJ([Lng, Lat]);
// const center1 = [position.value.lng, position.value.lat];
// const center2 = GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]);
const center1 = turf.point([lng, lat]);
const center2 = turf.point(GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]));
const dis = turf.distance(center1, center2, { units: 'meters' }).toFixed(1);
const tmp = (standardData.value?.value || 0) * 2;
if (Math.abs(tmp - dis) > 0.5) {
openToast('warn', '请确保误差处于50厘米内');
return;
}
}
createCircle(index);
const { circle1Lat, circle1Lng, circle2Lat, circle2Lng } = formData.value;
if (circle1Lat && circle1Lng && circle2Lng && circle2Lat) {
const center1 = GPS2GCJ([formData.value.circle1Lng, formData.value.circle1Lat]);
const center2 = GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]);
bearing.value = 90 - turf.bearing(center1, center2);
rotate.value = bearing.value;
showEight();
}
}
watch([() => formData.value.envGradeId, () => formData.value.licenseGradeId], () => {
const { circle1Lat, circle1Lng, circle2Lat, circle2Lng } = formData.value;
if (circle1Lat && circle1Lng && circle2Lng && circle2Lat) {
const center1 = GPS2GCJ([formData.value.circle1Lng, formData.value.circle1Lat]);
const center2 = GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]);
bearing.value = 90 - turf.bearing(center1, center2);
rotate.value = bearing.value;
showEight();
}
})
// function showEight() {
// const center1 = GPS2GCJ([formData.value.circle1Lng, formData.value.circle1Lat]);
// const center2 = GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]);
//
// // const center1 = [-122.3895127, 37.6280898];
// // const center2 = [-122.3894255, 37.6281725];
// console.log(center1, center2);
// const { polygons: shapePolygons, circles: shapeCircles, markers: shapeMarkers } = creatEightShaped({
// center: center1,
// radius: standardData.value.value,
// radiusDiff: standardDiffData.value.value,
// centerWidth: 0.1,
// }, {
// center: center2,
// radius: standardData.value.value,
// radiusDiff: standardDiffData.value.value,
// centerWidth: 0.1,
// },
// [6, 0, 1, 3, 4, 5, 2],
// { radius: point3standardData.value });
// extCircles.value = [...shapeCircles];
// polygons.value = [...shapePolygons];
// extMarker.value = [...shapeMarkers];
// // }
// }
const extCircles = ref([])
// const extCircles = ref([])
const extMarker = ref([])
</script>
<template>
<view :class="s.root">
<view class="mapBox">
<map
id="map"
:markers="[...markerPoints, ...markers, ...extMarker, ...distanceText]"
:longitude="center.lng"
:latitude="center.lat"
:polygons="polygons"
:polyline="[...polyline]"
:scale="20"
:circles="[...circles, ...extCircles]"
:enable-rotate="enableRotate"
:rotate="bearing"
:enable-satellite="true"
:show-compass="true"
/>
</view>
<RightSide @create="onHandleCreateCircle" />
<BottomSide :info="{ ...position, ...attitude, ...gps, ...battery, connectLoading, connectDrone, droneOnLine, sysStatus }"/>
</view>
<nut-toast :msg="state.msg" v-model:visible="state.show" :type="state.type" :cover="state.cover" :duration="2000" />
</template>
<style lang="less" module="s">
page {
height: 100%;
overflow: hidden;
}
.root {
position: relative;
width: 100%;
height: 100%;
:global {
.mapBox {
width: 100%;
height: 100%;
position: relative;
#map {
width: 100%;
height: 100%;
transform: scale(2);
}
.exam-result-modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
.modal-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 20px;
border-radius: 12px;
min-width: 280px;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.close-btn {
position: absolute;
top: 10px;
right: 10px;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
font-size: 20px;
color: #999;
cursor: pointer;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background-color: #f5f5f5;
color: #666;
}
}
.result-status {
font-size: 20px;
font-weight: bold;
text-align: center;
margin-bottom: 16px;
color: #ff4d4f;
&.pass {
color: #52c41a;
}
}
.info-item {
margin: 8px 0;
display: flex;
align-items: center;
.label {
color: #838383;
margin-right: 8px;
font-size: 14px;
}
.value {
color: #ffffff;
font-weight: 500;
font-size: 14px;
}
}
}
}
}
.real-time-data {
z-index: 1;
}
}
}
</style>

224
out/production/uav-edu-mp/src/pages/airfieldMap/useConnector.js

@ -0,0 +1,224 @@
import { createGlobalState, useWebSocket } from '@vueuse/core';
import { computed, ref, watch } from 'vue';
import * as turf from '@turf/turf';
import gcoord from 'gcoord';
import * as urls from '../config/urls';
import * as geo from '../utils/geo';
import { GPS_FIX_TYPE, GPS_FIX_TYPE2 } from '../config/gpsFixTypeMap';
import { FLY_MODE } from '../config/flyModeMap';
import { FC_SYSTEM_STATUS } from '../config/fcSystemStatus';
import { ERRORS_COUNT_1, ERRORS_COUNT_2, ERRORS_COUNT_3 } from '../config/errorMap';
import { useAnnouncer } from './useAnnouncer';
const announcer = useAnnouncer();
export const useConnector = createGlobalState(() => {
const { ws, status, data, send, open, close } = useWebSocket(urls.WS_URL, {
immediate: false,
heartbeat: {
interval: 3000,
},
});
watch(data, (val) => {
if (val === 'ping') {
send('pong');
}
});
const isConnecting = computed(() => status.value === 'CONNECTING');
const isConnected = computed(() => status.value === 'OPEN');
const time = ref({});
const battery = ref({});
const gps = ref({});
const position = ref({});
const attitude = ref({});
const home = ref({});
const homeAngle = ref(0);
const modeName = ref('N/A');
const sysStatus = ref('N/A');
const extra = ref({});
const info = computed(() => {
let result = data.value || '';
if (!`${data.value}`.startsWith('{')) {
return {};
}
try {
result = JSON.parse(result);
} catch (e) {
return {};
}
return result;
});
// 闲置计时器(闲置时做些动作)
let idleTimer = null;
watch(info, (val) => {
// info有变化时,清掉上一个计时器
if (idleTimer) clearTimeout(idleTimer);
// 电池信息、故障信息
// http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#22-%E7%B3%BB%E7%BB%9F%E7%8A%B6%E6%80%81-sys_status
if (val?.msgId === 1) {
const { VoltageBattery, CurrentBattery, BatteryRemaining, ErrorsCount1, ErrorsCount2, ErrorsCount3 } = val.data || {};
const voltage = +(VoltageBattery / 1e2 || 0).toFixed(1) || null; // 飞控给的电压是cV即厘伏(mavlink文档中写的是mV即毫伏)
const current = +(CurrentBattery / 1e2 || 0).toFixed(1) || null; // 飞控给的电压是cA即厘安
const remaining = BatteryRemaining >= 0 ? BatteryRemaining : null; // 单位:%
battery.value = { voltage, current, remaining };
// todo 临时
if (ErrorsCount1) {
const { [ErrorsCount1]: errorContent } = ERRORS_COUNT_1;
announcer.alarmThrottle(errorContent);
}
if (ErrorsCount2) {
const { [ErrorsCount2]: errorContent } = ERRORS_COUNT_2;
announcer.alarmThrottle(errorContent);
}
if (ErrorsCount3) {
const { [ErrorsCount3]: errorContent } = ERRORS_COUNT_3;
announcer.alarmThrottle(errorContent);
}
}
// 系统时间
// https://mavlink.io/en/messages/common.html#SYSTEM_TIME
if (val?.msgId === 2) {
const { TimeUnixUsec, TimeBootMs } = val.data || {};
const timestamp = parseInt(TimeUnixUsec / 1e3, 10); // to毫秒
const boot = TimeBootMs; // 毫秒
time.value = { timestamp, boot };
}
// GPS(with RTK)
// https://mavlink.io/en/messages/common.html#GPS2_RAW
if (val?.msgId === 124) {
const { FixType: fixType, SatellitesVisible: satellite } = val.data || {};
const fixTypeLabel = GPS_FIX_TYPE2.get(fixType);
gps.value = { fixType, fixTypeLabel, satellite };
}
// GPS(普通GPS)
// https://mavlink.io/en/messages/common.html#GPS_INPUT
if (val?.msgId === 232) {
if ('fixType' in gps.value) return;
const { FixType: fixType, SatellitesVisible: satellite } = val.data || {};
const { [fixType]: fixTypeLabel } = GPS_FIX_TYPE;
gps.value = { fixType, fixTypeLabel, satellite };
}
// 飞机位置
// http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#25-%E8%9E%8D%E5%90%88%E7%BB%8F%E7%BA%AC%E5%BA%A6%E9%80%9F%E5%BA%A6-global_position_int
if (val?.msgId === 33) {
const { Lon, Lat, Alt, RelativeAlt, Vx, Vy, Vz } = val.data || {};
const [lng, lat] = gcoord.transform([Lon / 1e7, Lat / 1e7], gcoord.WGS84, gcoord.GCJ02);
const alt = Alt / 1e3; // 源值毫米
const height = RelativeAlt / 1e3; // 源值毫米
const sx = (+Vx || 0) / 1e2; // 向北速度分量(源值厘米)
const sy = (+Vy || 0) / 1e2; // 向东速度分量(源值厘米)
const sz = (+Vz || 0) / 1e2; // 向下速度分量(源值厘米)
const hSpeed = Math.hypot(sx, sy);
const vSpeed = Math.abs(sz);
const { lng: hLng, lat: hLat } = home.value || {};
const homeDist = hLng !== undefined ? turf.distance([lng, lat], [hLng, hLat]) * 1e3 : null; // distance单位是km
position.value = { lng, lat, alt, height, hSpeed, vSpeed, sx, sy, sz, homeDist };
}
// 飞机姿态
// http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#26-%E9%A3%9E%E6%9C%BA%E5%A7%BF%E6%80%81%E8%A7%92%E9%80%9F%E5%BA%A6-attitude
if (val?.msgId === 30) {
const { Yaw, Pitch, Roll } = val.data || {};
const yaw = +geo.radToDeg(Yaw || 0).toFixed(1);
const pitch = +geo.radToDeg(Pitch || 0).toFixed(1);
const roll = +geo.radToDeg(Roll || 0).toFixed(1);
attitude.value = { yaw, pitch, roll };
}
// 油门
// https://mavlink.io/en/messages/common.html#VFR_HUD
if (val?.msgId === 74) {
const { Throttle } = val.data || {};
extra.value = { throttle: Throttle };
}
// home点
// http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#23-home%E7%82%B9-home_position
if (val?.msgId === 242) {
const { Longitude, Latitude } = val.data || {};
let lng = +(Longitude / 1e7).toFixed(7);
let lat = +(Latitude / 1e7).toFixed(7);
[lng, lat] = [lat, lng]; // todo 临时颠倒一下
home.value = { lng, lat };
}
// 飞行模式、解锁状态
// http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#110-%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A3%9E%E8%A1%8C%E6%A8%A1%E5%BC%8F-vkfly_custom_mode
// https://mavlink.io/en/messages/common.html#HEARTBEAT
if (val?.msgId === 0) {
const { CustomMode, SystemStatus } = val.data || {};
const { [CustomMode]: label } = FLY_MODE;
modeName.value = label || 'N/A';
sysStatus.value = FC_SYSTEM_STATUS.get(SystemStatus) || 'N/A';
}
// 1秒后,若info没有新的变化,则清空这些数据
idleTimer = setTimeout(() => {
time.value = {};
battery.value = {};
gps.value = {};
position.value = {};
attitude.value = {};
home.value = {};
modeName.value = 'N/A';
sysStatus.value = 'N/A';
}, 1000);
});
// 动态计算机头与home点的夹角
watch([position, attitude], () => {
const { lng: hLng, lat: hLat } = home.value || {};
if (hLng === undefined) {
homeAngle.value = 0;
return;
}
const { lng: dLng, lat: dLat } = position.value || {};
const { yaw } = attitude.value || {};
if (dLng === undefined || yaw === undefined) return;
const [lng, lat] = [hLng - dLng, hLat - dLat]; // 以飞机为起点,home为终点的向量
// 不能是0向量(飞机与home点完全重合时,则令机头始终指向home)
if (!lng && !lat) {
homeAngle.value = 0;
return;
}
const rad = geo.angleWithYAxis([lng, lat]); // 与(0,1)这个单位向量的夹角
const deg = geo.radToDeg(rad) * (lng >= 0 ? 1 : -1); // 通过x轴正负来决定角度正负
homeAngle.value = +(deg - yaw).toFixed(2) || 0;
});
return {
ws,
isConnecting,
isConnected,
time,
battery,
gps,
position,
attitude,
home,
homeAngle,
modeName,
sysStatus,
extra,
send,
connect: open,
reconnect: open,
close,
};
});
export default null;

331
out/production/uav-edu-mp/src/pages/airfieldMap/useDroneMarker.js

@ -0,0 +1,331 @@
import {computed, onMounted, ref} from 'vue';
import deviceIcon from "../../assets/droneImg.png";
import droneDisImg from '../../assets/droneDisImg.png';
import Taro from "@tarojs/taro";
import {storeToRefs} from "pinia";
import {useAirFieldsStore, useStandardStore, useSupervisionStore} from "../../stores";
import {GCJ2GPS, GPS2GCJ} from "../../utils/helpers";
import {creatEightShaped} from "../flightMap/utils";
import * as turf from "@turf/turf";
import transparentImg from "../../assets/transparent-marker.png";
const { formData } = storeToRefs(useAirFieldsStore());
const { examList, envList } = storeToRefs(useStandardStore());
const envItem = computed(() => {
return envList.value.find((item) => item.id === formData.value.envGradeId) || {};
});
const standardData = computed(() => {
return examList.value.find((item) => item.name === '8字圆圈半径' && item.licenseLevelId === formData.value.licenseGradeId && item.envGrade === envItem.value.gradeName) || {};
});
const standardDiffData = computed(() => {
return examList.value.find((item) => item.name === '8字水平偏差阈值' && item.licenseLevelId === formData.value.licenseGradeId && item.envGrade === envItem.value.gradeName) || {};
});
const point3standardData = computed(() => {
return examList.value.find((item) => item.name === '点3中心筒范围-内' && item.licenseLevelId === formData.value.licenseGradeId && item.envGrade === envItem.value.gradeName) || {};
});
export function useDroneMarker() {
let mapContext;
const { position: Position, attitude, droneOnLine } = storeToRefs(useSupervisionStore());
const position = computed(() => {
const { lng: Lng, lat: Lat } = Position.value || {};
const [lng, lat] = GPS2GCJ([Lng, Lat]);
return {
...(Position.value || {}),
lng,
lat,
}
});
const rotate = ref(0);
const extMarker = ref([]);
const markers = computed(() => {
if (!Object.keys(position.value).length || !mapContext) return [...extMarker.value];
return [{
id: 1e7,
iconPath: droneOnLine.value ? deviceIcon : droneDisImg,
width: 18,
height: 18,
anchor: { x: 0.5, y: 0.5 },
latitude: position.value?.lat,
longitude: position.value?.lng,
rotate: attitude.value?.yaw + rotate.value,
}, ...extMarker.value]
})
const circle1 = ref([]);
const circle2 = ref([]);
const currentCircle = computed(() => {
if (!Object.keys(position.value).length || !mapContext) return [];
return [{
latitude: position.value?.lat,
longitude: position.value?.lng,
color: '#00d9ff',
radius: standardData.value.value || 6,
// fillColor: '#00000000',
strokeWidth: 0.8
}]
});
const extCircles = ref([]);
const circles = computed(() => [...circle1.value, ...circle2.value, ...currentCircle.value, ...extCircles.value]);
const polygons = ref([]);
const distanceText = computed(() => {
if (!droneOnLine.value) {
return []
}
if (circle1.value.length) {
const center1 = turf.point([position.value?.lng, position.value?.lat]);
const center2 = turf.point([circle1.value[0]?.longitude, circle1.value[0]?.latitude]);
const text = turf.distance(center1, center2, { units: 'meters' }).toFixed(1);
const [lng, lat] = turf.midpoint(center1, center2).geometry.coordinates
return [
{
id: 1e7 + 9,
latitude: lat,
longitude: lng,
iconPath: transparentImg,
width: 1,
height: 1,
label: {
content: `${text || 0} m`,
color: '#000000',
fontSize: 8,
// textStrokeWidth: 2,
// textStrokeColor: '#007fcf',
anchorX: -3,
anchorY: 0,
bgColor: '#00000000'
}
}
]
}
if (circle2.value.length) {
const center1 = turf.point([position.value?.lng, position.value?.lat]);
const center2 = turf.point([circle2.value[0]?.longitude, circle2.value[0]?.latitude]);
const text = turf.distance(center1, center2, { units: 'meters' }).toFixed(1);
const [lng, lat] = turf.midpoint(center1, center2).geometry.coordinates;
return [
{
id: 1e7 + 9,
latitude: lat,
longitude: lng,
iconPath: transparentImg,
width: 1,
height: 1,
label: {
content: `${text || 0} m`,
color: '#000000',
fontSize: 8,
// textStrokeWidth: 2,
// textStrokeColor: '#007fcf',
anchorX: -3,
anchorY: 0,
bgColor: '#00000000'
}
}
]
}
return [];
})
const polyline = computed(() => {
if (!droneOnLine.value) {
return []
}
if (circle1.value.length) {
return [
{
points: [
{
latitude: position.value?.lat,
longitude: position.value?.lng,
},
{
latitude: circle1.value[0]?.latitude,
longitude: circle1.value[0]?.longitude,
},
],
color: '#FF0000',
width: 0.8,
dottedLine: true,
level:'abovebuildings',
}
]
}
if (circle2.value.length) {
return [
{
points: [
{
latitude: position.value?.lat,
longitude: position.value?.lng,
},
{
latitude: circle2.value[0]?.latitude,
longitude: circle2.value[0]?.longitude,
},
],
color: '#FF0000',
width: 0.8,
dottedLine: true,
level:'abovebuildings',
// segmentTexts: [{
// name: 'ccccc',
// startIndex: 0,
// endIndex: 1,
// }],
// textStyle: {
// fontSize: 20,
// textColor: '#000000'
// }
}
]
}
return [];
})
onMounted(() => {
mapContext = Taro.createMapContext('map');
})
let callBack = () => {};
function createCircle(index = 1) {
const { lat, lng } = position.value || {};
if (index === 1) {
circle1.value = [
{
latitude: lat,
longitude: lng,
color: '#FF0000',
radius: standardData.value.value || 6,
// fillColor: '#00000000',
strokeWidth: 0.8
},
{
latitude: lat,
longitude: lng,
color: '#FF0000',
radius: 0.1,
fillColor: '#FF0000',
strokeWidth: 0
},
];
const [Lng, Lat] = GCJ2GPS([lng, lat]);
formData.value.circle1Lat = Lat;
formData.value.circle1Lng = Lng;
callBack(index);
}
if (index === 2) {
circle2.value = [
{
latitude: lat,
longitude: lng,
color: '#FF0000',
radius: standardData.value.value || 6,
// fillColor: '#00000000',
strokeWidth: 0.8
},
{
latitude: lat,
longitude: lng,
color: '#FF0000',
radius: 0.1,
fillColor: '#FF0000',
strokeWidth: 0
},
]
const [Lng, Lat] = GCJ2GPS([lng, lat]);
formData.value.circle2Lat = Lat;
formData.value.circle2Lng = Lng;
callBack(index);
}
}
function setCallBack(fn = () => {}) {
callBack = fn;
}
// function moveDevice(point, mapRotate = 0) {
// const { lng, lat, yaw } = point;
//
// map.translateMarker({
// markerId,
// destination: {
// longitude: lng,
// latitude: lat,
// },
// autoRotate: false,
// duration: 1,
// rotate: yaw + mapRotate,
// moveWithRotate: true,
// animationEnd: () => {
// // this._that.timelyData = { ...nextData };
// // this._that.currentIndex += 1;
// // if (this._that.isPlaying) {
// // this._that.elapsedMs += (duration * this._that.speedRate);
// // if (this._that.elapsedMs >= this._that.totalTime) {
// // this._that.handleStop();
// // return;
// // }
// // this._that._renderDevice();
// // }
// }
// });
// }
// const polyine = ref();
// function renderTrack(points = []) {
//
// }
function showEight() {
const center1 = GPS2GCJ([formData.value.circle1Lng, formData.value.circle1Lat]);
const center2 = GPS2GCJ([formData.value.circle2Lng, formData.value.circle2Lat]);
// const center1 = [-122.3895127, 37.6280898];
// const center2 = [-122.3894255, 37.6281725];
// console.log(center1, center2);
const { polygons: shapePolygons, circles: shapeCircles, markers: shapeMarkers } = creatEightShaped({
center: center1,
radius: standardData.value.value,
radiusDiff: standardDiffData.value.value,
centerWidth: 0.1,
}, {
center: center2,
radius: standardData.value.value,
radiusDiff: standardDiffData.value.value,
centerWidth: 0.1,
},
[6, 0, 1, 3, 4, 5, 2],
{ radius: point3standardData.value.value });
extCircles.value = [...shapeCircles];
polygons.value = [...shapePolygons];
extMarker.value = [...shapeMarkers];
// }
}
return {
// initDevice,
// moveDevice,
markers,
rotate,
circles,
polygons,
createCircle,
showEight,
setCallBack,
polyline,
distanceText,
}
}

6
out/production/uav-edu-mp/src/pages/evaluation/index.config.js

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '自我讲评',
disableSwipeBack: true,
enablePullDownRefresh: true,
backgroundTextStyle: 'dark',
})

628
out/production/uav-edu-mp/src/pages/evaluation/index.vue

@ -0,0 +1,628 @@
<script setup>
import { ref, onMounted, reactive } from 'vue';
import { useEvaluationStore, useAuthStore } from '../../stores';
import { storeToRefs } from 'pinia';
import Taro from '@tarojs/taro';
import { formatTime } from '../../utils/helpers';
import { Uploader } from '@nutui/icons-vue-taro';
const { getEvaluationList, getEvaluationDetail, replyEvaluation, updateEvaluation, createEvaluation } = useEvaluationStore();
const { evaluationList, evaluationExtra, evaluationQueries } = storeToRefs(useEvaluationStore());
const { isTeacher, isStudent } = storeToRefs(useAuthStore());
const replyContent = ref('');
const isReplying = ref(false);
const editContent = ref('');
const isEditing = ref(false);
const showEdit = ref(false);
const showCreate = ref(false);
const createContent = ref('');
const isCreating = ref(false);
const list = ref([])
const state = reactive({
msg: '错误提示',
type: 'warn',
show: false,
cover: true,
center: true,
});
function openToast(type = 'warn', msg = '错误提示') {
state.msg = msg;
state.type = type;
state.show = true;
}
const loading = ref(false);
const finished = ref(false);
const showDetail = ref(false);
const showReply = ref(false);
const currentDetail = ref(null);
const currentEvaluation = ref(null);
const isRefreshing = ref(false);
function onRefresh() {
isRefreshing.value = true;
evaluationQueries.value.pageNum = 1;
getEvaluationList().then(() => {
list.value = [...evaluationList.value];
Taro.stopPullDownRefresh();
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
}).finally(() => {
isRefreshing.value = false;
});
}
onRefresh();
function onLoadMore() {
if (loading.value) return;
if (list.value.length >= evaluationExtra.value.total) return;
loading.value = true;
evaluationQueries.value.pageNum += 1;
getEvaluationList().then(() => {
list.value = [...list.value, ...evaluationList.value];
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
}).finally(() => {
loading.value = false;
});
}
function viewDetail(id) {
getEvaluationDetail(id).then(({ data }) => {
currentDetail.value = data;
showDetail.value = true;
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
});
}
function showReplyDialog(item) {
currentEvaluation.value = item;
replyContent.value = '';
isReplying.value = false;
showReply.value = true;
}
function submitReply() {
if (!replyContent.value.trim()) {
openToast('warn', '请输入教评内容');
return;
}
isReplying.value = true;
replyEvaluation(currentEvaluation.value.id, replyContent.value)
.then(() => {
openToast('success', '教评成功');
replyContent.value = '';
showReply.value = false;
onRefresh();
})
.catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
.finally(() => {
isReplying.value = false;
});
}
function showEditDialog(item) {
currentEvaluation.value = item;
editContent.value = item.content;
isEditing.value = false;
showEdit.value = true;
}
function submitEdit() {
if (!editContent.value.trim()) {
openToast('warn', '请输入评论内容');
return;
}
isEditing.value = true;
updateEvaluation({ id: currentEvaluation.value.id, content: editContent.value })
.then(() => {
openToast('success', '修改成功');
editContent.value = '';
showEdit.value = false;
// onRefresh();
})
.catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
.finally(() => {
isEditing.value = false;
});
}
function submitCreate() {
if (!createContent.value.trim()) {
openToast('warn', '请输入自我讲评内容');
return;
}
isCreating.value = true;
createEvaluation({ content: createContent.value })
.then(() => {
openToast('success', '创建成功');
createContent.value = '';
showCreate.value = false;
// onRefresh();
})
.catch(({ msg }) => {
if (msg) openToast('warn', msg);
})
.finally(() => {
isCreating.value = false;
});
}
function formatDate(date) {
return formatTime(date, 'YYYY-MM-DD');
}
onMounted(() => {
getEvaluationList().catch(({ msg }) => {
if (msg) openToast('warn', msg);
});
});
Taro.usePullDownRefresh(() => {
onRefresh();
});
Taro.useReachBottom(() => {
onLoadMore();
});
</script>
<template>
<div :class="s.root">
<div class="list">
<div v-for="item in list" :key="item.id" class="item" :class="{ 'has-reply': item.hasReply }">
<div class="header">
<div class="student-name">{{ item.studentName || '匿名' }}</div>
<div class="status" v-if="!item.hasReply" :class="{ 'has-reply': item.hasReply }">{{ item.hasReply ? '已回复' : '未回复' }}</div>
</div>
<div class="content">{{ item.content }}</div>
<div class="footer">
<div class="date">{{ formatTime(item.createTime) }}</div>
<div class="buttons">
<nut-button size="mini" type="info" @click="viewDetail(item.id)">
<span>查看回复</span>
</nut-button>
<nut-button v-if="isStudent" size="mini" type="primary" @click="showEditDialog(item)">
<span>修改</span>
</nut-button>
<nut-button v-if="isTeacher" size="mini" type="primary" @click="showReplyDialog(item)">
<span>写教评</span>
</nut-button>
</div>
</div>
</div>
<nut-empty v-if="!loading && !list.length" description="暂无数据" />
<nut-infiniteloading
v-if="list.length"
load-txt="加载中..."
load-more-txt="没有更多了"
:has-more="list.length < evaluationExtra.total"
@load-more="onLoadMore"
/>
</div>
<div class="create-button" v-if="isStudent" @click="showCreate = true">
<nut-button type="info"><Uploader /></nut-button>
</div>
<nut-popup v-model:visible="showDetail" position="bottom" :style="{ height: '70%' }" round>
<div class="detail" v-if="currentDetail">
<div class="main-content">
<div class="content">{{ currentDetail.content }}</div>
<div class="footer">
<div class="student-name">{{ currentDetail.studentName }}</div>
<div class="time">{{ formatTime(currentDetail.createTime) }}</div>
</div>
</div>
<div class="replies">
<div class="reply-title">回复列表</div>
<div v-if="currentDetail.replies && currentDetail.replies.length > 0" class="reply-list">
<div v-for="reply in currentDetail.replies" :key="reply.createTime" class="reply-item">
<div class="reply-content">{{ reply.content }}</div>
<div class="reply-info">
<span class="teacher">{{ reply.teacherName || '老师' }}</span>
<span class="time">{{ formatTime(reply.createTime) }}</span>
</div>
</div>
</div>
<nut-empty v-else description="暂无回复" />
</div>
</div>
</nut-popup>
<nut-popup v-model:visible="showReply" position="bottom" :style="{ height: '40%' }" round>
<div class="reply-popup">
<div class="reply-popup-header">
<div class="title">教评</div>
<div class="close" @click="showReply = false">
<i class="iconfont icon-close"></i>
</div>
</div>
<div class="reply-popup-content">
<nut-textarea
v-model="replyContent"
placeholder="请输入回复内容"
:maxlength="100"
:rows="4"
/>
<nut-button
block
type="primary"
:loading="isReplying"
@click="submitReply"
>提交回复</nut-button>
</div>
</div>
</nut-popup>
<nut-popup v-model:visible="showEdit" position="bottom" :style="{ height: '40%' }" round>
<div class="reply-popup">
<div class="reply-popup-header">
<div class="title">修改评论</div>
<div class="close" @click="showEdit = false">
<i class="iconfont icon-close"></i>
</div>
</div>
<div class="reply-popup-content">
<nut-textarea
v-model="editContent"
placeholder="请输入评论内容"
:maxlength="100"
:rows="4"
/>
<nut-button
block
type="primary"
:loading="isEditing"
@click="submitEdit"
>提交修改</nut-button>
</div>
</div>
</nut-popup>
<nut-toast :msg="state.msg" v-model:visible="state.show" :type="state.type" :cover="state.cover" :center="state.center" />
<nut-popup v-model:visible="showCreate" position="bottom" :style="{ height: '40%' }" round>
<div class="reply-popup">
<div class="reply-popup-header">
<div class="title">新建自我讲评</div>
<div class="close" @click="showCreate = false">
<i class="iconfont icon-close"></i>
</div>
</div>
<div class="reply-popup-content">
<nut-textarea
v-model="createContent"
placeholder="请输入自我讲评内容"
:maxlength="100"
:rows="4"
/>
<nut-button
block
type="primary"
:loading="isCreating"
@click="submitCreate"
>提交</nut-button>
</div>
</div>
</nut-popup>
</div>
</template>
<style lang="less" module="s">
.root {
box-sizing: border-box;
min-height: 100vh;
background-color: #f5f5f5;
padding: 16px;
:global {
.list {
.item {
background-color: #fff;
border-radius: 16px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
position: relative;
overflow: visible;
z-index: 0;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
background-color: #838383;
border-radius: 16px 0 0 16px;
z-index: 2;
transition: all 0.3s ease;
}
&.has-reply::before {
background-color: #00c691;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.student-name {
font-size: 16px;
color: #262626;
font-weight: 500;
}
.status {
font-size: 14px;
color: #8c8c8c;
padding: 4px 12px;
border-radius: 20px;
background-color: #f5f5f5;
&.has-reply {
color: #00c691;
background-color: rgba(0, 198, 145, 0.1);
}
}
}
.content {
font-size: 24px;
line-height: 1.5;
color: #262626;
font-weight: 500;
margin-bottom: 16px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
.date {
font-size: 14px;
color: #8c8c8c;
}
.buttons {
display: flex;
gap: 12px;
.nut-button {
height: 36px;
// padding: 0 16px;
border-radius: 20px;
// color: #00c691;
// border-color: #00c691;
// background-color: rgba(0, 198, 145, 0.1);
// &:hover {
// background-color: rgba(0, 198, 145, 0.2);
// }
// i {
// margin-right: 6px;
font-size: 16px;
// }
}
}
}
}
}
.loading, .no-more {
text-align: center;
padding: 16px;
color: #999;
font-size: 14px;
}
.detail {
pointer-events: none;
padding: 24px;
// height: 100%;
overflow-y: auto;
.main-content {
background-color: #f8f8f8;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
.content {
font-size: 24px;
line-height: 1.5;
color: #262626;
font-weight: 500;
margin-bottom: 16px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
.student-name {
font-size: 16px;
color: #262626;
font-weight: 500;
}
.time {
font-size: 14px;
color: #8c8c8c;
}
}
}
.replies {
.reply-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
color: #262626;
padding-left: 12px;
border-left: 4px solid #00c691;
}
.reply-list {
.reply-item {
background-color: #f8f8f8;
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
transition: all 0.3s ease;
&:hover {
background-color: #f0f0f0;
}
.reply-content {
font-size: 16px;
line-height: 1.6;
color: #262626;
margin-bottom: 12px;
}
.reply-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
.teacher {
color: #00c691;
font-weight: 500;
}
.time {
color: #8c8c8c;
}
}
}
}
}
}
.reply-popup {
padding: 20px;
// height: 100%;
display: flex;
flex-direction: column;
.reply-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title {
// font-size: 18px;
font-weight: bold;
color: #262626;
}
.close {
padding: 8px;
cursor: pointer;
color: #8c8c8c;
transition: all 0.3s ease;
&:hover {
color: #262626;
}
i {
font-size: 20px;
}
}
}
.reply-popup-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 20px 20px;
height: calc(100% - 60px);
overflow-y: auto;
.nut-textarea {
margin-bottom: 20px;
background-color: #f8f8f8;
border-radius: 12px;
padding: 16px;
font-size: 16px;
min-height: 120px;
}
.nut-button {
// height: 48px;
border-radius: 12px;
// font-size: 16px;
// background-color: #00c691;
// border-color: #00c691;
margin-top: auto;
// &:active {
// background-color: darken(#00c691, 10%);
// border-color: darken(#00c691, 10%);
// }
}
}
}
.create-button {
position: fixed;
left: 24px;
bottom: 10vh;
// width: 56px;
// height: 56px;
border-radius: 50%;
// background-color: #00c691;
// box-shadow: 0 4px 12px rgba(0, 198, 145, 0.3);
// display: flex;
// align-items: center;
// justify-content: center;
// cursor: pointer;
// transition: all 0.3s ease;
// z-index: 100;
// &:hover {
// transform: translateY(-2px);
// // box-shadow: 0 6px 16px rgba(0, 198, 145, 0.4);
// }
.nut-button {
border-radius: 50%;
}
i {
width: 10px;
height: 24px;
// font-size: 24px;
color: #fff;
}
}
}
}
</style>

6
out/production/uav-edu-mp/src/pages/flight/index.config.js

@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '实践飞行',
disableSwipeBack: true,
enablePullDownRefresh: true,
backgroundTextStyle: 'dark',
})

244
out/production/uav-edu-mp/src/pages/flight/index.vue

@ -0,0 +1,244 @@
<script setup>
import { reactive, ref } from 'vue';
import Taro from "@tarojs/taro";
import { storeToRefs } from "pinia";
import { useFlightStore } from "../../stores";
const { getFlightList } = useFlightStore();
const { flightList, flightExtra, flightQueries } = storeToRefs(useFlightStore());
const list = ref([])
const state = reactive({
msg: '错误提示',
type: 'warn',
show: false,
cover: true,
center: true,
});
function openToast(type = 'warn', msg = '错误提示') {
state.msg = msg;
state.type = type;
state.show = true;
}
const loading = ref(false);
const isRefreshing = ref(false);
function onRefresh() {
isRefreshing.value = true;
flightQueries.value.pageNum = 1;
getFlightList().then(() => {
list.value = [...flightList.value];
Taro.stopPullDownRefresh();
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
}).finally(() => {
isRefreshing.value = false;
});
}
onRefresh();
function onLoadMore() {
if (loading.value) return;
if (list.value.length >= flightExtra.value.total) return;
loading.value = true;
flightQueries.value.pageNum += 1;
getFlightList().then(() => {
list.value = [...list.value, ...flightList.value];
}).catch(({ msg }) => {
if (msg) openToast('warn', msg);
}).finally(() => {
loading.value = false;
});
}
function onNavTo(recordId) {
Taro.navigateTo({
url: `/pages/flightMap2/index?recordId=${recordId}`,
});
}
Taro.usePullDownRefresh(() => {
onRefresh();
});
Taro.useReachBottom(() => {
onLoadMore();
});
// Taro.useDidShow(() => {
// getFlightList().catch(({ msg }) => {
// if (msg) openToast('warn', msg);
// });
// });
</script>
<template>
<div :class="s.root">
<div class="list">
<template v-for="item in list">
<div class="item" v-if="item?.isPass === false || item?.isPass === true" :key="item.recordId" :class="{ 'is-pass': item.isPass }">
<div class="title">
<span>{{ item.studentName || '-' }}</span>
<nut-button size="mini" type="info" @click="onNavTo(item.recordId)">回放</nut-button>
</div>
<div class="info">
<div class="info-row">
<span>班级{{ item.className || '-' }}</span>
<span>无人机{{ item.droneName || '-' }}</span>
</div>
<div class="info-row">
<span>教员{{ item.teacherName || '-' }}</span>
</div>
<div class="info-row">
<span>场地{{ item.airfieldName || '-' }}</span>
</div>
<div class="info-row">
<span>开始时间{{ item.startTime || '-' }}</span>
</div>
<div class="info-row">
<span>结束时间{{ item.endTime || '-' }}</span>
</div>
<div class="info-row">
<span>
<nut-tag v-if="item?.isPass === false || item?.isPass === true" :type="item.isPass ? 'success' : 'danger'">
{{ item.isPass ? '通过' : '未通过' }}
</nut-tag>
<nut-tag v-else type="warning">
训练中...
</nut-tag>
</span>
<span>失败次数{{ item.failTimes || 0 }}</span>
</div>
</div>
</div>
</template>
<nut-empty v-if="!loading && !list.length" description="暂无数据" />
<nut-infiniteloading
v-if="list.length"
load-txt="加载中..."
load-more-txt="没有更多了"
:has-more="list.length < flightExtra.total"
@load-more="onLoadMore"
/>
</div>
</div>
<nut-toast :msg="state.msg" v-model:visible="state.show" :type="state.type" :cover="state.cover" :duration="2000" />
</template>
<style lang="less" module="s">
page {
height: 100%;
padding: 20px 0;
box-sizing: border-box;
background-color: #eaeaea;
overflow: auto;
}
.root {
height: 100%;
display: flex;
flex-direction: column;
:global {
.list {
//flex: 1;
//overflow: auto;
padding: 0 20px;
.item {
background-color: white;
border-radius: 16px;
padding: 28px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
background-color: #4CAF50;
opacity: 0.8;
}
&:not(.is-pass)::before {
background-color: #FF5252;
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
&:active {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
}
& + .item {
margin-top: 28px;
}
.title {
font-size: 38px;
font-weight: 600;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
color: #2c3e50;
:global(.nut-button) {
font-size: 28px;
padding: 10px 24px;
border-radius: 8px;
background: rgba(74, 144, 226, 0.12);
color: #4a90e2;
border: none;
font-weight: 500;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
opacity: 0.9;
}
}
}
.info {
color: #666;
font-size: 28px;
line-height: 1.6;
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
padding-right: 12px;
&:last-child {
margin-bottom: 0;
}
span {
color: #34495e;
&:first-child {
color: #7f8c8d;
}
}
}
}
}
}
}
}
</style>

113
out/production/uav-edu-mp/src/pages/flightMap/BottomSide.vue

@ -0,0 +1,113 @@
<script setup>
import { computed, ref } from 'vue';
import { Left, Right } from '@nutui/icons-vue-taro';
// import { toFixed } from '@/utils/helpers.js';
import { toFixed } from '../../utils/helpers';
// import deviceCruise from '../../../core/deviceCruise';
import { IconFont } from "@nutui/icons-vue-taro";
import {useFlightStore} from "../../stores";
import {storeToRefs} from "pinia";
import dayjs from "dayjs";
import { popularTime } from '../../utils/helpers';
// import Taro from "@tarojs/taro";
const { flightDetail } = storeToRefs(useFlightStore());
// const isVisible = ref(true);
// const info = computed(() => ({}));
const sum = computed(() => {
const { startTime, endTime } = flightDetail.value || {};
if (startTime && endTime) {
//
const sTime = dayjs(startTime); //
const eTime = dayjs(endTime);
//
const diffSeconds = eTime.diff(sTime, 'second');
return popularTime(diffSeconds, 0, true);
}
return '-';
})
function onBack() {
console.log('aaa');
}
</script>
<template>
<div :class="s.root">
<canvas class="canvas-bg" type="2d" />
<div class="item item1">
<div class="key">开始时间</div>
<div class="time">{{ flightDetail?.startTime }}</div>
</div>
<div class="item item2">
<div class="key">结束时间</div>
<div class="time">{{ flightDetail?.endTime }}</div>
</div>
<div class="item item3">
<div class="key">考试耗时</div>
<div class="time">{{ sum }}</div>
</div>
</div>
</template>
<style lang="less" module="s">
.root {
pointer-events: visible;
position: absolute;
left: 0;
bottom: 0;
right: 0;
//bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
// padding: 8px;
color: white;
font-size: 11px;
//width: 100px;
height: 30px;
overflow: hidden;
box-sizing: content-box;
z-index: 2;
display: flex;
:global {
.canvas-bg {
width: 80%;
height: 50px;
position: absolute;
//top: 0;
right: 0;
//left: 0;
//bottom: 20px;
}
.item {
display: flex;
align-items: center;
justify-content: center;
+ .item {
border-left: 2px solid white;
}
.time {
margin-left: 25px;
}
}
.item1 {
flex: 1;
}
.item2 {
flex: 1;
}
.item3 {
width: 150px;
}
}
}
</style>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save