@ -1,6 +1,6 @@ |
|||||
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config |
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config |
||||
# TARO_APP_ID="开发环境下的小程序 AppID" |
# 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" |
||||
|
|
||||
|
|
||||
|
@ -1,3 +1,3 @@ |
|||||
# TARO_APP_ID="生产环境下的小程序 AppID" |
# 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" |
||||
|
@ -1,6 +1,6 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
<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" /> |
<output url="file://$PROJECT_DIR$/out" /> |
||||
</component> |
</component> |
||||
</project> |
</project> |
@ -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 |
@ -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" |
||||
|
|
||||
|
|
@ -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" |
@ -0,0 +1 @@ |
|||||
|
# TARO_APP_ID="测试环境下的小程序 AppID" |
@ -0,0 +1,5 @@ |
|||||
|
// ESLint 检查 .vue 文件需要单独配置编辑器: |
||||
|
// https://eslint.vuejs.org/user-guide/#editor-integrations |
||||
|
{ |
||||
|
"extends": ["taro/vue3"] |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
dist/ |
||||
|
deploy_versions/ |
||||
|
.temp/ |
||||
|
.rn_temp/ |
||||
|
node_modules/ |
||||
|
.DS_Store |
||||
|
.swc |
||||
|
*.local |
@ -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 |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="VcsDirectoryMappings"> |
||||
|
<mapping directory="" vcs="Git" /> |
||||
|
</component> |
||||
|
</project> |
@ -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 |
||||
|
}] |
||||
|
] |
||||
|
} |
@ -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'] |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
module.exports = { |
||||
|
env: { |
||||
|
NODE_ENV: '"development"' |
||||
|
}, |
||||
|
defineConstants: {}, |
||||
|
mini: {}, |
||||
|
h5: {} |
||||
|
} |
@ -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')) |
||||
|
} |
@ -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') })
|
||||
|
// }))
|
||||
|
// }
|
||||
|
} |
||||
|
} |
@ -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" |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
} |
@ -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" |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"miniprogramRoot": "./", |
||||
|
"projectname": "uav-edu-mp", |
||||
|
"description": "", |
||||
|
"appid": "touristappid", |
||||
|
"setting": { |
||||
|
"urlCheck": true, |
||||
|
"es6": false, |
||||
|
"postcss": false, |
||||
|
"minified": false |
||||
|
}, |
||||
|
"compileType": "miniprogram" |
||||
|
} |
@ -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"]
|
||||
|
}) |
@ -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; |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 86 B |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 941 B |
After Width: | Height: | Size: 174 B |
@ -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"; |
||||
|
} |
||||
|
|
After Width: | Height: | Size: 688 B |
After Width: | Height: | Size: 473 B |
After Width: | Height: | Size: 942 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 185 B |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 103 B |
After Width: | Height: | Size: 146 B |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -0,0 +1,97 @@ |
|||||
|
/** |
||||
|
* 故障报警 |
||||
|
* 参考1: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
|
||||
|
* 参考2:http://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和磁偏航角差异过大' |
||||
|
}, |
||||
|
}; |
@ -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()]; |
@ -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 {}; |
@ -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: '精密单点定位', |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
export const STAGE_MAP = { |
||||
|
spin: { |
||||
|
text: '自旋', |
||||
|
}, |
||||
|
shape8: { |
||||
|
text: '八字飞行', |
||||
|
}, |
||||
|
hover: { |
||||
|
text: '悬停', |
||||
|
}, |
||||
|
} |
@ -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: '操作失败,飞行方向错误', |
||||
|
} |
||||
|
} |
@ -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`; |
@ -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; |
@ -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; |
@ -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, |
||||
|
} |
@ -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, |
||||
|
}; |
@ -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, |
||||
|
} |
@ -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", |
||||
|
} |
@ -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();
|
@ -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();
|
@ -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 |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
||||
|
} |
@ -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, |
||||
|
} |
||||
|
} |
@ -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,} |
||||
|
} |
@ -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, |
||||
|
} |
||||
|
} |
@ -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> |
@ -0,0 +1,9 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '场地管理', |
||||
|
// disableSwipeBack: true,
|
||||
|
// enablePullDownRefresh: true,
|
||||
|
// backgroundTextStyle: 'dark',
|
||||
|
// navigationStyle: 'custom',
|
||||
|
// enablePullDownRefresh: false,
|
||||
|
// disableScroll: false,
|
||||
|
}) |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -0,0 +1,118 @@ |
|||||
|
/** |
||||
|
* 地理空间函数库 |
||||
|
*/ |
||||
|
import * as math from 'mathjs'; |
||||
|
import * as turf from '@turf/turf'; |
||||
|
|
||||
|
/** |
||||
|
* 将两个点连成一个向量(并确保落在1、4象限) |
||||
|
* @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; |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '场地', |
||||
|
navigationStyle: 'custom', |
||||
|
pageOrientation: 'landscape' |
||||
|
}) |
@ -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> |
@ -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; |
@ -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, |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '自我讲评', |
||||
|
disableSwipeBack: true, |
||||
|
enablePullDownRefresh: true, |
||||
|
backgroundTextStyle: 'dark', |
||||
|
}) |
@ -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> |
@ -0,0 +1,6 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '实践飞行', |
||||
|
disableSwipeBack: true, |
||||
|
enablePullDownRefresh: true, |
||||
|
backgroundTextStyle: 'dark', |
||||
|
}) |
@ -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> |
@ -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> |