@ -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://canola-tool.jiagutech.com/api" |
||||
|
TARO_APP_API="http://192.168.10.23:9761" |
||||
|
|
||||
|
|
@ -0,0 +1,3 @@ |
|||||
|
# TARO_APP_ID="生产环境下的小程序 AppID" |
||||
|
# TARO_APP_API="https://canola-tool.jiagutech.com/api" |
||||
|
TARO_APP_API="http://192.168.10.23:9761" |
@ -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"> |
||||
|
<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,9 @@ |
|||||
|
<?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$" /> |
||||
|
<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,36 @@ |
|||||
|
/* 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'] |
||||
|
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'] |
||||
|
NutInfiniteloading: typeof import('@nutui/nutui-taro')['Infiniteloading'] |
||||
|
NutInput: typeof import('@nutui/nutui-taro')['Input'] |
||||
|
NutOverlay: typeof import('@nutui/nutui-taro')['Overlay'] |
||||
|
NutPopup: typeof import('@nutui/nutui-taro')['Popup'] |
||||
|
NutRange: typeof import('@nutui/nutui-taro')['Range'] |
||||
|
NutTabbar: typeof import('@nutui/nutui-taro')['Tabbar'] |
||||
|
NutTabbarItem: typeof import('@nutui/nutui-taro')['TabbarItem'] |
||||
|
NutTag: typeof import('@nutui/nutui-taro')['Tag'] |
||||
|
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] |
||||
|
NutToast: typeof import('@nutui/nutui-taro')['Toast'] |
||||
|
RealTimeData: typeof import('./src/components/RealTimeData.vue')['default'] |
||||
|
RoutePointInfo: typeof import('./src/components/RoutePointInfo.vue')['default'] |
||||
|
SupervisionData: typeof import('./src/components/SupervisionData.vue')['default'] |
||||
|
SupervisionSideData: typeof import('./src/components/SupervisionSideData.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,86 @@ |
|||||
|
{ |
||||
|
"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-ui/miniapp": "1.0.7", |
||||
|
"@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", |
||||
|
"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,31 @@ |
|||||
|
{ |
||||
|
"miniprogramRoot": "dist/", |
||||
|
"projectname": "uav-edu-mp", |
||||
|
"description": "", |
||||
|
"appid": "wxa55106c0b120722e", |
||||
|
"setting": { |
||||
|
"urlCheck": true, |
||||
|
"es6": true, |
||||
|
"enhance": true, |
||||
|
"compileHotReLoad": false, |
||||
|
"postcss": false, |
||||
|
"minified": true, |
||||
|
"babelSetting": { |
||||
|
"ignore": [], |
||||
|
"disablePlugins": [], |
||||
|
"outputPath": "" |
||||
|
} |
||||
|
}, |
||||
|
"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.7.12" |
||||
|
} |
@ -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,22 @@ |
|||||
|
export default defineAppConfig({ |
||||
|
pages: [ |
||||
|
'pages/login/index', |
||||
|
'pages/supervisionMap/index', |
||||
|
'pages/supervision/index', |
||||
|
'pages/flightMap/index', |
||||
|
'pages/returnTripMap/index', |
||||
|
'pages/routePlanMap/index', |
||||
|
'pages/home/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' |
||||
|
}, |
||||
|
}) |
@ -0,0 +1,21 @@ |
|||||
|
import { createApp } from 'vue'; |
||||
|
import store from './stores'; |
||||
|
// import './app.less';
|
||||
|
import './assets/iconfont.css' |
||||
|
// import './common.less';
|
||||
|
import { IconFont } from '@nutui/icons-vue-taro'; |
||||
|
|
||||
|
|
||||
|
const App = createApp({ |
||||
|
onShow(options) { |
||||
|
console.log('App Show', options); |
||||
|
|
||||
|
}, |
||||
|
// 入口组件不需要实现 render 方法,即使实现了也会被 taro 所覆盖
|
||||
|
}) |
||||
|
|
||||
|
App.use(store); |
||||
|
App.use(IconFont); |
||||
|
|
||||
|
|
||||
|
export default App; |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 941 B |
After Width: | Height: | Size: 174 B |
@ -0,0 +1,27 @@ |
|||||
|
@font-face { |
||||
|
font-family: "FontAwesome"; /* Project id 4868745 */ |
||||
|
src: url('./iconfont.woff2?t=1742801623512') format('woff2'), |
||||
|
url('./iconfont.woff?t=1742801623512') format('woff'), |
||||
|
url('./iconfont.ttf?t=1742801623512') format('truetype'); |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
font-family: FontAwesome; |
||||
|
font-size: 16px; |
||||
|
font-style: normal; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-moz-osx-font-smoothing: grayscale; |
||||
|
} |
||||
|
|
||||
|
.icon-stop:before { |
||||
|
content: "\e6b5"; |
||||
|
} |
||||
|
|
||||
|
.icon-pause:before { |
||||
|
content: "\e7fe"; |
||||
|
} |
||||
|
|
||||
|
.icon-play:before { |
||||
|
content: "\e6cf"; |
||||
|
} |
||||
|
|
After Width: | Height: | Size: 688 B |
After Width: | Height: | Size: 942 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 185 B |
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,206 @@ |
|||||
|
<script setup> |
||||
|
import { ref } from 'vue'; |
||||
|
import { Left, Right } from '@nutui/icons-vue-taro'; |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
isPassed: { |
||||
|
type: Boolean, |
||||
|
required: true |
||||
|
}, |
||||
|
errorPoints: { |
||||
|
type: Array, |
||||
|
default: () => [ |
||||
|
{ |
||||
|
current: '85°', |
||||
|
qualified: '90°', |
||||
|
message: '方位角偏离预定航线过大' |
||||
|
}, |
||||
|
{ |
||||
|
current: '120米', |
||||
|
qualified: '100米', |
||||
|
message: '飞行高度超出安全范围' |
||||
|
}, |
||||
|
{ |
||||
|
current: '12米/秒', |
||||
|
qualified: '8米/秒', |
||||
|
message: '飞行速度过快,不符合考试要求' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const isVisible = ref(true); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="[s.root, { [s.hidden]: !isVisible }]"> |
||||
|
<div class="toggle-btn" :class="{ 'is-active': !isVisible }" @click="() => isVisible = !isVisible"> |
||||
|
<Right v-if="isVisible" style="font-size: 12px;" /> |
||||
|
<Left style="font-size: 12px;" v-else /> |
||||
|
</div> |
||||
|
<div class="title"> |
||||
|
<div style="display: flex; align-items: center; gap: 4px;"> |
||||
|
<div>考试结果</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="content"> |
||||
|
<div class="result-info"> |
||||
|
<div class="status" :class="{ 'passed': isPassed, 'failed': !isPassed }"> |
||||
|
{{ isPassed ? '通过' : '未通过' }} |
||||
|
</div> |
||||
|
<div v-if="!isPassed && errorPoints.length > 0" class="error-points"> |
||||
|
<div class="error-title">错误点位信息:</div> |
||||
|
<div v-for="(point, index) in errorPoints" :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); |
||||
|
border-radius: 8px; |
||||
|
color: white; |
||||
|
font-size: 10px; |
||||
|
width: 12vw; |
||||
|
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; |
||||
|
|
||||
|
.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-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,205 @@ |
|||||
|
<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 deviceCruise from '../core/deviceCruise'; |
||||
|
|
||||
|
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(() => deviceCruise.timelyData); |
||||
|
</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,222 @@ |
|||||
|
<script setup> |
||||
|
import { computed, ref } from 'vue'; |
||||
|
import { Left, Right } from '@nutui/icons-vue-taro'; |
||||
|
import { toFixed } from '../utils/helpers'; |
||||
|
|
||||
|
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({ |
||||
|
// isVisible: { |
||||
|
// type: Boolean, |
||||
|
// default: true |
||||
|
// }, |
||||
|
pointInfo: { |
||||
|
type: Object, |
||||
|
required: true, |
||||
|
default: () => ({ |
||||
|
index: 1, |
||||
|
azimuth: 0, |
||||
|
distance: 0, |
||||
|
height: 0, |
||||
|
stayTime: 0 |
||||
|
}) |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// const emit = defineEmits(['update:isVisible']); |
||||
|
|
||||
|
// const toggleVisibility = () => { |
||||
|
// emit('update:isVisible', !props.isVisible); |
||||
|
// }; |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="[s.root, { [s.hidden]: !isVisible }]"> |
||||
|
<div class="title"> |
||||
|
<div style="display: flex; align-items: center; gap: 4px;"> |
||||
|
<div>航线点信息</div> |
||||
|
</div> |
||||
|
<div class="toggle-btn" :class="{ '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="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">{{ toFixed(item.angle, 1) }}°</span> |
||||
|
</div> |
||||
|
<div class="detail-row"> |
||||
|
<span class="label">相对距离</span> |
||||
|
<span class="value">{{ toFixed(item.distance, 2) }}米</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.stayTime }}秒</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); |
||||
|
border-radius: 8px; |
||||
|
color: white; |
||||
|
font-size: 10px; |
||||
|
width: fit-content; |
||||
|
min-width: 17vw; |
||||
|
z-index: 2; |
||||
|
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; |
||||
|
|
||||
|
.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,324 @@ |
|||||
|
<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 deviceCruise from '../core/deviceCruise'; |
||||
|
|
||||
|
const isVisible = ref(true); |
||||
|
const info = computed(() => deviceCruise.timelyData); |
||||
|
|
||||
|
// 计算飞行时间 |
||||
|
const flightTime = computed(() => { |
||||
|
const startTime = deviceCruise.startTime; |
||||
|
if (!startTime) return '00:00:00'; |
||||
|
const duration = Math.floor((Date.now() - startTime) / 1000); |
||||
|
const hours = Math.floor(duration / 3600); |
||||
|
const minutes = Math.floor((duration % 3600) / 60); |
||||
|
const seconds = duration % 60; |
||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
||||
|
}); |
||||
|
|
||||
|
// 错误点列表 |
||||
|
const errorPoints = ref([ |
||||
|
// 示例数据 |
||||
|
// { position: { lat: 39.908692, lng: 116.397477 }, type: '高度超限', time: '2024-01-20 10:30:00' } |
||||
|
]); |
||||
|
</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="container"> |
||||
|
<!-- 基本状态 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">基本状态</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">电量:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ info.battery || 0 }}</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">{{ info.satellites || 0 }}</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">{{ flightTime }}</small> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 位置信息 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">位置信息</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"> |
||||
|
<span class="label">高度:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ toFixed(info.altitude, 2) }}</small> |
||||
|
<span class="unit">米</span> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 姿态信息 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">姿态信息</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">俯仰角:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ toFixed(info.pitch, 2) }}</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.roll, 2) }}</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.yaw, 2) }}</small> |
||||
|
<span class="unit">°</span> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 偏移信息 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">偏移信息</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">切线角:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ toFixed(info.tangentAngle, 2) }}</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.horizontalOffset, 2) }}</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.verticalOffset, 2) }}</small> |
||||
|
<span class="unit">米</span> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 错误点列表 --> |
||||
|
<div class="section" v-if="errorPoints.length > 0"> |
||||
|
<div class="section-title">错误点列表</div> |
||||
|
<div class="error-list"> |
||||
|
<div v-for="(point, index) in errorPoints" :key="index" class="error-item"> |
||||
|
<div class="error-type">{{ point.type }}</div> |
||||
|
<div class="error-position"> |
||||
|
<small>{{ toFixed(point.position.lat, 7) }}, {{ toFixed(point.position.lng, 7) }}</small> |
||||
|
</div> |
||||
|
<div class="error-time"> |
||||
|
<small>{{ point.time }}</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.root { |
||||
|
pointer-events: visible; |
||||
|
position: absolute; |
||||
|
left: 0px; |
||||
|
top: 0px; |
||||
|
bottom: 0px; |
||||
|
background-color: rgba(0, 0, 0, 0.6); |
||||
|
padding: 8px; |
||||
|
border-radius: 8px; |
||||
|
color: white; |
||||
|
font-size: 11px; |
||||
|
width: fit-content; |
||||
|
min-width: 140px; |
||||
|
z-index: 2; |
||||
|
transition: transform 0.3s ease; |
||||
|
|
||||
|
&.hidden { |
||||
|
transform: translateX(calc(-100% - 5px)); |
||||
|
} |
||||
|
|
||||
|
:global { |
||||
|
.container { |
||||
|
// display: flex; |
||||
|
// flex-direction: column; |
||||
|
// gap: 12px; |
||||
|
box-sizing: border-box; |
||||
|
overflow: auto; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.section { |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 12px; |
||||
|
font-weight: 500; |
||||
|
color: #fff; |
||||
|
margin-bottom: 6px; |
||||
|
padding-bottom: 4px; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.text-row { |
||||
|
margin-bottom: 4px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 2px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.label { |
||||
|
min-width: 50px; |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.error-list { |
||||
|
.error-item { |
||||
|
margin-bottom: 8px; |
||||
|
padding: 6px; |
||||
|
background-color: rgba(255, 0, 0, 0.2); |
||||
|
border-radius: 4px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.error-type { |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 2px; |
||||
|
} |
||||
|
|
||||
|
.error-position, |
||||
|
.error-time { |
||||
|
font-size: 0.85em; |
||||
|
opacity: 0.8; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,214 @@ |
|||||
|
<script setup> |
||||
|
import { computed, ref } from 'vue'; |
||||
|
import { Left, Right } from '@nutui/icons-vue-taro'; |
||||
|
import deviceCruise from '../core/deviceCruise'; |
||||
|
|
||||
|
const isVisible = ref(true); |
||||
|
const info = computed(() => deviceCruise.timelyData); |
||||
|
|
||||
|
// 学员信息 |
||||
|
const studentInfo = ref({ |
||||
|
name: '张三', // 示例数据 |
||||
|
droneId: 'UAV-001', // 示例数据 |
||||
|
}); |
||||
|
|
||||
|
// 阶段性倒计时 |
||||
|
const countdown = ref({ |
||||
|
phase: '起飞阶段', // 示例数据 |
||||
|
remainingTime: '05:00', // 示例数据 |
||||
|
}); |
||||
|
|
||||
|
// 实时语音播报内容 |
||||
|
const voiceContent = ref('请注意保持安全高度'); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="[s.root, { [s.hidden]: !isVisible }]"> |
||||
|
<div class="nav"> |
||||
|
<div :class="{ 'toggle-btn': true, 'is-active': !isVisible }" @click="() => isVisible = !isVisible"> |
||||
|
<Right v-if="isVisible" style="font-size: 14px;" /> |
||||
|
<Left style="font-size: 14px;" v-else /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="container"> |
||||
|
<div class="container-2"> |
||||
|
<!-- 学员信息 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">学员信息</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">姓名:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ studentInfo.name }}</small> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">飞机编号:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ studentInfo.droneId }}</small> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 阶段信息 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">阶段信息</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">当前阶段:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ countdown.phase }}</small> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="text-row"> |
||||
|
<span class="label">剩余时间:</span> |
||||
|
<span class="value"> |
||||
|
<div class="value-unit"> |
||||
|
<small class="value">{{ countdown.remainingTime }}</small> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 语音播报 --> |
||||
|
<div class="section"> |
||||
|
<div class="section-title">语音播报</div> |
||||
|
<div class="voice-content"> |
||||
|
{{ voiceContent }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.root { |
||||
|
pointer-events: visible; |
||||
|
position: absolute; |
||||
|
right: 0px; |
||||
|
top: 0px; |
||||
|
bottom: 0px; |
||||
|
background-color: rgba(0, 0, 0, 0.6); |
||||
|
padding: 8px; |
||||
|
border-radius: 8px; |
||||
|
color: white; |
||||
|
font-size: 11px; |
||||
|
width: fit-content; |
||||
|
min-width: 140px; |
||||
|
z-index: 2; |
||||
|
transition: transform 0.3s ease; |
||||
|
|
||||
|
&.hidden { |
||||
|
transform: translateX(calc(100% + 5px)); |
||||
|
} |
||||
|
|
||||
|
:global { |
||||
|
.container { |
||||
|
// box-sizing: border-box; |
||||
|
overflow: auto; |
||||
|
height: 100%; |
||||
|
|
||||
|
.container-2 { |
||||
|
// display: flex; |
||||
|
// flex-direction: column; |
||||
|
// gap: 8px; |
||||
|
// padding: 4px; |
||||
|
height: 102%; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.nav { |
||||
|
position: absolute; |
||||
|
left: -20px; |
||||
|
top: 6px; |
||||
|
|
||||
|
.toggle-btn { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
cursor: pointer; |
||||
|
background-color: rgba(0, 0, 0, 0.6); |
||||
|
border-radius: 4px 0 0 4px; |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.section { |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 12px; |
||||
|
font-weight: 500; |
||||
|
color: #fff; |
||||
|
margin-bottom: 6px; |
||||
|
padding-bottom: 4px; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.text-row { |
||||
|
margin-bottom: 4px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 2px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.label { |
||||
|
min-width: 50px; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
small { |
||||
|
font-size: 0.85em; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.voice-content { |
||||
|
padding: 6px; |
||||
|
background-color: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 4px; |
||||
|
font-size: 0.85em; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,54 @@ |
|||||
|
<script setup> |
||||
|
import { eventCenter, getCurrentInstance, reLaunch } from '@tarojs/taro' |
||||
|
import {onMounted, ref, h, computed} from "vue"; |
||||
|
import { 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(Category), path: '/pages/home/index' }, |
||||
|
{ title: '监管', icon: h(Eye), path: '/pages/supervision/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(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,195 @@ |
|||||
|
<script setup> |
||||
|
import { computed, ref } from 'vue'; |
||||
|
import deviceCruise from '../core/deviceCruise'; |
||||
|
import { PlayStop, PlayStart, Refresh } from '@nutui/icons-vue-taro'; |
||||
|
|
||||
|
// 计算属性 |
||||
|
const ready = computed(() => deviceCruise.ready); |
||||
|
const speedRate = computed(() => deviceCruise.speedRate); |
||||
|
const totalTime = computed(() => deviceCruise.totalTime); |
||||
|
const currentTime = computed({ |
||||
|
get() { |
||||
|
return deviceCruise.elapsedMs; |
||||
|
}, |
||||
|
set(val) { |
||||
|
// console.log('aaaaa', val); |
||||
|
// deviceCruise.handlePause(); |
||||
|
deviceCruise.setCurrentTime(val); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const showPlayButton = computed(() => deviceCruise.isPaused || deviceCruise.isStopped); |
||||
|
|
||||
|
// 格式化时间显示 |
||||
|
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(deviceCruise.elapsedMs)); |
||||
|
const totalTimeDisplay = computed(() => formatTime(totalTime.value)); |
||||
|
|
||||
|
// 事件处理函数 |
||||
|
function onChangeSpeedRate() { |
||||
|
const newSpeedRate = speedRate.value >= 8 ? 1 : speedRate.value * 2; |
||||
|
deviceCruise.setSpeedRate(newSpeedRate); |
||||
|
} |
||||
|
|
||||
|
function onPlay() { |
||||
|
deviceCruise.handlePlay(); |
||||
|
} |
||||
|
|
||||
|
function onPause() { |
||||
|
deviceCruise.handlePause(); |
||||
|
} |
||||
|
|
||||
|
function onStop() { |
||||
|
deviceCruise.handleStop(); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<view :class="s.root"> |
||||
|
<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 class="controls"> |
||||
|
<view class="btn" @click="onChangeSpeedRate" style="color: #fff;"> |
||||
|
{{ speedRate }} x |
||||
|
</view> |
||||
|
<view class="btn" v-if="showPlayButton" @click="onPlay"> |
||||
|
<PlayStart /> |
||||
|
</view> |
||||
|
<view class="btn" v-else @click="onPause"> |
||||
|
<PlayStop /> |
||||
|
</view> |
||||
|
<view class="btn" @click="onStop"> |
||||
|
<Refresh /> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.root { |
||||
|
pointer-events: visible; |
||||
|
position: absolute; |
||||
|
left: 10rpx; |
||||
|
right: 10rpx; |
||||
|
bottom: 5rpx; |
||||
|
z-index: 999; |
||||
|
background: rgba(0, 0, 0, 0.5); |
||||
|
padding: 10rpx 15rpx 10rpx 15rpx; |
||||
|
border-radius: 8rpx; |
||||
|
display: flex; |
||||
|
// flex-direction: row; |
||||
|
align-items: center; |
||||
|
gap: 20rpx; |
||||
|
|
||||
|
:global { |
||||
|
.slider-container { |
||||
|
flex: 4; |
||||
|
|
||||
|
.nut-range-button { |
||||
|
width: 16rpx; |
||||
|
height: 16rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.time-display { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
font-size: 16rpx; |
||||
|
color: #fff; |
||||
|
margin-top: 5rpx; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
.controls { |
||||
|
// flex: 2; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
// justify-content: flex-end; |
||||
|
gap: 10rpx; |
||||
|
|
||||
|
.btn { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
color: #fff; |
||||
|
background-color: #0069ca!important; |
||||
|
min-width: 30rpx; |
||||
|
font-size: 16rpx; |
||||
|
height: 40rpx; |
||||
|
width: 40rpx; |
||||
|
border-radius: 8rpx; |
||||
|
|
||||
|
&:active { |
||||
|
background-color: #0056b3!important; |
||||
|
} |
||||
|
|
||||
|
.nut-icon { |
||||
|
//width: 12px; |
||||
|
//height: 12px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.play-controls { |
||||
|
// display: flex; |
||||
|
// gap: 10rpx; |
||||
|
} |
||||
|
|
||||
|
// .nut-button { |
||||
|
// min-width: 30rpx; |
||||
|
// height: 30rpx; |
||||
|
// padding: 0 6rpx; |
||||
|
// display: flex; |
||||
|
// align-items: center; |
||||
|
// justify-content: center; |
||||
|
// color: #fff; |
||||
|
// // background-color: #1890ff !important; |
||||
|
// } |
||||
|
|
||||
|
.iconfont { |
||||
|
font-size: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.time-display { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
font-size: 16rpx; |
||||
|
color: #fff; |
||||
|
margin-top: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.icon-play:before { |
||||
|
content: '\e87c'; |
||||
|
} |
||||
|
|
||||
|
.icon-pause:before { |
||||
|
content: '\e87d'; |
||||
|
} |
||||
|
|
||||
|
.icon-stop:before { |
||||
|
content: '\e87e'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,96 @@ |
|||||
|
/** |
||||
|
* 接口地址列表 |
||||
|
*/ |
||||
|
import { buildURL } from '../utils/helpers'; |
||||
|
const { TARO_APP_API: BASE_URL } = process.env; // 获取环境变量
|
||||
|
// const { TARO_APP_API: BASE_URL } = import.meta; // 获取环境变量
|
||||
|
// 上传
|
||||
|
// export const UPLOAD = `${BASE_URL}/v1/files/upload`;
|
||||
|
export const UPLOAD = `http://192.168.10.23:9762/v1/files/upload`; |
||||
|
|
||||
|
// 用户登录
|
||||
|
export const LOGIN_WITH_PASSWORD = `${BASE_URL}/user/token`; |
||||
|
export const WECHAT_AUTH_URL = `${BASE_URL}/wechat/mini/auth`; |
||||
|
export const GET_WECHAT_USERINFO = `${BASE_URL}/wechat/mini/getWxUserInfo`; |
||||
|
|
||||
|
//学生信息
|
||||
|
export const UPDATE_STUDENT = (id) => buildURL(`${BASE_URL}/v1/students/{id}`, id); |
||||
|
|
||||
|
//人员信息
|
||||
|
export const UPDATE_MEMBER = (id) => buildURL(`${BASE_URL}/v1/users/{id}`, id); |
||||
|
|
||||
|
// 讲评管理
|
||||
|
// export const GET_EVALUATION_LIST = `${BASE_URL}/v1/evaluations`;
|
||||
|
// export const CREATE_EVALUATION = `${BASE_URL}/v1/evaluations`;
|
||||
|
// export const UPDATE_EVALUATION = `${BASE_URL}/v1/evaluations`;
|
||||
|
// export const GET_EVALUATION_DETAIL = (id) => buildURL(`${BASE_URL}/v1/evaluations/{id}`, id);
|
||||
|
|
||||
|
export const GET_EVALUATION_LIST = `http://192.168.10.23:9762/v1/evaluations`; |
||||
|
export const CREATE_EVALUATION = `http://192.168.10.23:9762/v1/evaluations`; |
||||
|
export const UPDATE_EVALUATION = `http://192.168.10.23:9762/v1/evaluations`; |
||||
|
export const GET_EVALUATION_DETAIL = (id) => buildURL(`http://192.168.10.23:9762/v1/evaluations/{id}`, id); |
||||
|
export const REPLY_EVALUATION = 'http://192.168.10.23:9762/v1/evaluations/reply'; |
||||
|
|
||||
|
// 实践飞行管理
|
||||
|
// export const GET_FLIGHT_LIST = `${BASE_URL}/v1/flightRecords`;
|
||||
|
// export const GET_FLIGHT_DETAIL = (id) => buildURL(`${BASE_URL}/v1/flightRecords/{id}`, id);
|
||||
|
// export const GET_FLIGHT_TRACKS = (id) => buildURL(`${BASE_URL}/v1/flightRecords/{id}/tracks`, id);
|
||||
|
|
||||
|
export const GET_FLIGHT_LIST = `http://192.168.10.23:9762/v1/flightRecords`; |
||||
|
export const GET_FLIGHT_DETAIL = (id) => buildURL(`http://192.168.10.23:9762/v1/flightRecords/{id}`, id); |
||||
|
export const GET_FLIGHT_TRACKS = (id) => buildURL(`http://192.168.10.23:9762/v1/flightRecords/{id}/tracks`, id); |
||||
|
|
||||
|
// 航线管理
|
||||
|
// export const GET_ROUTE_PLAN_LIST = `${BASE_URL}/v1/routePlans`;
|
||||
|
// export const GET_ROUTE_PLAN_DETAIL = (id) => buildURL(`${BASE_URL}/v1/routePlans/{id}`, id);
|
||||
|
|
||||
|
export const GET_ROUTE_PLAN_LIST = `http://192.168.10.23:9762/v1/routePlans`; |
||||
|
export const GET_ROUTE_PLAN_DETAIL = (id) => buildURL(`http://192.168.10.23:9762/v1/routePlans/{id}`, id); |
||||
|
|
||||
|
// 应急返航管理
|
||||
|
// export const GET_RETURN_TRIP_LIST = `${BASE_URL}/v1/returnTrips`;
|
||||
|
// export const GET_RETURN_TRIP_DETAIL = (id) => buildURL(`${BASE_URL}/v1/returnTrips/{id}`, id);
|
||||
|
|
||||
|
export const GET_RETURN_TRIP_LIST = `http://192.168.10.23:9762/v1/returnTrips`; |
||||
|
export const GET_RETURN_TRIP_DETAIL = (id) => buildURL(`http://192.168.10.23:9762/v1/returnTrips/{id}`, id); |
||||
|
|
||||
|
|
||||
|
// 用户信息
|
||||
|
export const GET_USER_INFO = `${BASE_URL}/user/userInfo`; |
||||
|
|
||||
|
// 识别记录
|
||||
|
export const CREATE_RECORD = `${BASE_URL}/record/createRecord`; |
||||
|
|
||||
|
// export const CREATE_RECORD = `${BASE_URL}/record/upload`;
|
||||
|
export const GET_RECORD_LIST = `${BASE_URL}/record/page`; |
||||
|
export const DELETE_RECORD = `${BASE_URL}/record/delete/{recordId}`; |
||||
|
export const UPDATE_RECORD = `${BASE_URL}/record/update`; |
||||
|
export const GET_RECORD_DETAIL = `${BASE_URL}/record/getRecordDetail/{recordId}`; |
||||
|
export const UPDATE_RECORD_DETAIL = `${BASE_URL}/record/updateRecordContent`; |
||||
|
export const EXPORT_RECORD = `${BASE_URL}/record/export/{recordId}`; |
||||
|
export const SET_SCALE = `${BASE_URL}/record/scale`; |
||||
|
|
||||
|
// 任务
|
||||
|
export const GET_TASK_LIST = `${BASE_URL}/record/tasks`; |
||||
|
export const GET_TASK_PROGRESS = `${BASE_URL}/record/taskProgress/{taskId}`; |
||||
|
|
||||
|
// 模型列表
|
||||
|
export const GET_MODEL_LIST = `${BASE_URL}/record/models`; |
||||
|
export const UPDATE_USER = `${BASE_URL}/user/update`; |
||||
|
export const SEARCH_REGION_FARMER = `${BASE_URL}/user/listFarmerByRegionCode`; |
||||
|
export const UPDATE_FARMER = `${BASE_URL}/user/updateFarmer`; |
||||
|
|
||||
|
// 作业
|
||||
|
export const GET_WORK_RECORD_LIST = `${BASE_URL}/job/page`; |
||||
|
export const GET_TRACK_LIST = `${BASE_URL}/job/tracks/{jobId}`; |
||||
|
export const GET_WORK_TYPE_LIST = `${BASE_URL}/job/types`; |
||||
|
export const GET_WORKING_DETAIL = `${BASE_URL}/job/inWorking`; |
||||
|
export const UPDATE_WORK_STATUS = `${BASE_URL}/job/status`; |
||||
|
export const CREATE_WORK = `${BASE_URL}/job/start`; |
||||
|
|
||||
|
// 设备
|
||||
|
export const GET_DEVICE_LIST = `${BASE_URL}/device/page`; |
||||
|
export const GET_ONLINE_DEVICES = `${BASE_URL}/device/onlineAndRound`; |
||||
|
|
||||
|
// 地区
|
||||
|
export const GET_REGION_CHILDREN = `${BASE_URL}/region/getChildren`; |
@ -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,296 @@ |
|||||
|
/** |
||||
|
* 设备按轨迹巡航 |
||||
|
*/ |
||||
|
import { reactive, ref } from 'vue'; |
||||
|
import { interpolate } from 'popmotion'; |
||||
|
// import * as turf from '@turf/turf';
|
||||
|
import deviceIcon from '../assets/deviceIcon.png'; |
||||
|
|
||||
|
const markerId = 999; |
||||
|
|
||||
|
class DeviceCruise { |
||||
|
|
||||
|
_markerOptins = { |
||||
|
id: markerId, |
||||
|
iconPath: deviceIcon, |
||||
|
width: 32, |
||||
|
height: 32, |
||||
|
anchor: { x: 0.5, y: 0.5 } |
||||
|
} |
||||
|
_that = null; |
||||
|
|
||||
|
// 地图实例
|
||||
|
_map = null; |
||||
|
|
||||
|
// 单条轨迹数据
|
||||
|
_dataSource = {}; |
||||
|
|
||||
|
// 巡航速率
|
||||
|
speedRate = 1; |
||||
|
|
||||
|
// 动画计时器
|
||||
|
_timer = null; |
||||
|
|
||||
|
// 上一帧时间点
|
||||
|
_lastFrameAt = 0; |
||||
|
|
||||
|
// 累计播放时长(毫秒数)
|
||||
|
elapsedMs = 0; |
||||
|
|
||||
|
// 每帧期望间隔(毫秒数,实际间隔取决于浏览器fps)
|
||||
|
_fpsInterval = 1000 / 30; |
||||
|
|
||||
|
// 上一帧时间戳
|
||||
|
_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() { |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
// 载入轨迹数据(point中必须包含lng, lat, timestamp, yaw)
|
||||
|
loadTrack({ id, points, ...others }) { |
||||
|
// if (!this._that._that._map) {
|
||||
|
// throw new Error('请先设置地图实例');
|
||||
|
// }
|
||||
|
this._that._dataSource = { |
||||
|
id, |
||||
|
points: (points || []).map(item => ({ ...item })), |
||||
|
...others, |
||||
|
}; |
||||
|
this._that._reset(); |
||||
|
this._that._initDevice(); |
||||
|
} |
||||
|
|
||||
|
// 设置速率
|
||||
|
setSpeedRate(val) { |
||||
|
this._that.speedRate = val; |
||||
|
} |
||||
|
|
||||
|
_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 || 0, |
||||
|
}], |
||||
|
success: () => { |
||||
|
this._that._setting = false; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 获取指定毫秒处的数据值
|
||||
|
_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; |
||||
|
} |
||||
|
|
||||
|
_initDevice() { |
||||
|
const [point] = this._that._points; |
||||
|
if (!point) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const { lng, lat, yaw } = point; |
||||
|
this._that._map.addMarkers({ |
||||
|
markers: [{ |
||||
|
...this._that._markerOptins, |
||||
|
latitude: lat, |
||||
|
longitude: lng, |
||||
|
rotate: yaw || 0, |
||||
|
}] |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
_clearDevice() { |
||||
|
// 清除timelyData即可
|
||||
|
this._that.timelyData = {}; |
||||
|
this._that._map.removeMarkers({ |
||||
|
markerIds: [markerId] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
_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 || 0, |
||||
|
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();
|
||||
|
// }
|
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
_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,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,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,6 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '自我讲评', |
||||
|
disableSwipeBack: true, |
||||
|
enablePullDownRefresh: true, |
||||
|
backgroundTextStyle: 'dark', |
||||
|
}) |
@ -0,0 +1,621 @@ |
|||||
|
<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 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(() => { |
||||
|
Taro.stopPullDownRefresh(); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
isRefreshing.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onLoadMore() { |
||||
|
if (loading.value) return; |
||||
|
if (evaluationList.value.length >= evaluationExtra.value.total) return; |
||||
|
loading.value = true; |
||||
|
evaluationQueries.value.pageNum += 1; |
||||
|
getEvaluationList().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 evaluationList" :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="evaluationList.length === 0" description="暂无数据" /> |
||||
|
<nut-infiniteloading |
||||
|
v-if="evaluationList.length" |
||||
|
load-txt="加载中..." |
||||
|
load-more-txt="没有更多了" |
||||
|
:has-more="evaluationList.length < evaluationExtra.total" |
||||
|
@load-more="loadData" |
||||
|
/> |
||||
|
</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,226 @@ |
|||||
|
<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 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(() => { |
||||
|
Taro.stopPullDownRefresh(); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
isRefreshing.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onLoadMore() { |
||||
|
if (loading.value) return; |
||||
|
if (flightList.value.length >= flightExtra.value.total) return; |
||||
|
loading.value = true; |
||||
|
flightQueries.value.pageNum += 1; |
||||
|
getFlightList().catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onNavTo(id) { |
||||
|
Taro.navigateTo({ |
||||
|
url: `/pages/flightMap/index?id=${id}`, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
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"> |
||||
|
<div class="item" v-for="item in flightList" :key="item.id" :class="{ 'is-pass': item.isPass }"> |
||||
|
<div class="title"> |
||||
|
<span>{{ item.studentName || '-' }}</span> |
||||
|
<nut-button size="mini" type="info" @click="onNavTo(item.id)">回放</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><nut-tag :type="item.isPass ? 'success' : 'danger'">{{ item.isPass ? '通过' : '未通过' }}</nut-tag></span> |
||||
|
<span>失败次数:{{ item.failTimes || 0 }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<nut-empty v-if="!loading && !flightList.length" description="暂无数据" /> |
||||
|
<nut-infiniteloading |
||||
|
v-if="flightList.length" |
||||
|
load-txt="加载中..." |
||||
|
load-more-txt="没有更多了" |
||||
|
:has-more="flightList.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: hidden; |
||||
|
} |
||||
|
|
||||
|
.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,5 @@ |
|||||
|
export default { |
||||
|
navigationBarTitleText: '实践飞行回放', |
||||
|
navigationStyle: 'default', |
||||
|
pageOrientation: 'landscape' |
||||
|
}; |
@ -0,0 +1,252 @@ |
|||||
|
<script setup> |
||||
|
import { onMounted, ref, watch, onUnmounted, computed } from "vue"; |
||||
|
import * as Taro from "@tarojs/taro"; |
||||
|
import * as turf from '@turf/turf'; |
||||
|
import { creatEightShaped, convertToTrajectory } from './utils'; |
||||
|
import trackData from './track.json'; |
||||
|
import deviceCruise from '../../core/deviceCruise'; |
||||
|
import TrackPlayback from '../../components/TrackPlayback.vue'; |
||||
|
import RealTimeData from '../../components/RealTimeData.vue'; |
||||
|
|
||||
|
// Taro.hideHomeButton(); |
||||
|
|
||||
|
// 地图中心点位置 |
||||
|
const centerPoint = ref({ |
||||
|
lat: 39.908692, |
||||
|
lng: 116.397477 |
||||
|
}); |
||||
|
|
||||
|
// 地图上下文 |
||||
|
let mapContext = null; |
||||
|
|
||||
|
const trajectory = convertToTrajectory(trackData, { |
||||
|
startTime: "2025-03-14T08:00:00Z", |
||||
|
interval: 1000 // 1秒间隔 |
||||
|
}); |
||||
|
|
||||
|
const show = ref(false); |
||||
|
|
||||
|
// 初始化地图 |
||||
|
onMounted(() => { |
||||
|
mapContext = Taro.createMapContext('map'); |
||||
|
show.value = true; |
||||
|
console.log(mapContext); |
||||
|
|
||||
|
initEightShaped(); |
||||
|
initDeviceCruise(); |
||||
|
}); |
||||
|
|
||||
|
// 八字飞行实例 |
||||
|
let eightShaped = null; |
||||
|
|
||||
|
// 多边形配置 |
||||
|
const polygons = ref([]); |
||||
|
// 圆形标记配置 |
||||
|
const circles = ref([]); |
||||
|
// 标记点配置 |
||||
|
const markerPoints = ref([]); |
||||
|
// 飞行轨迹配置 |
||||
|
const polyline = ref([{ |
||||
|
points: trackData.map(coord => ({ |
||||
|
latitude: coord[1], |
||||
|
longitude: coord[0] |
||||
|
})), |
||||
|
color: '#008000', |
||||
|
width: 4, |
||||
|
arrowLine: true |
||||
|
}]); |
||||
|
|
||||
|
// 地图旋转角度 |
||||
|
const bearing = ref(0); |
||||
|
|
||||
|
// 初始化八字飞行 |
||||
|
function initEightShaped() { |
||||
|
if (!mapContext) return; |
||||
|
|
||||
|
const center1 = [106.630154, 26.647661]; |
||||
|
const center2 = [106.632154, 26.649661]; |
||||
|
|
||||
|
// 计算两个圆心之间的方位角并转换为地图旋转角度 |
||||
|
let angle = turf.bearing(center1, center2); |
||||
|
// 在横屏模式下,需要加上90度使八字保持水平 |
||||
|
// 将角度转换为0-360度范围 |
||||
|
angle = ((angle % 360) + 360) % 360; |
||||
|
|
||||
|
const { polygons: shapePolygons, circles: shapeCircles, markers: shapeMarkers } = creatEightShaped({ |
||||
|
center: center1, |
||||
|
radius: 105, |
||||
|
radiusDiff: 35, |
||||
|
centerWidth: 2, |
||||
|
}, { |
||||
|
center: center2, |
||||
|
radius: 105, |
||||
|
radiusDiff: 35, |
||||
|
centerWidth: 2, |
||||
|
}, [6, 0, 1, 3, 4, 5, 2], { radius: 50 }); |
||||
|
|
||||
|
// 更新地图显示数据 |
||||
|
polygons.value = shapePolygons; |
||||
|
markerPoints.value = shapeMarkers; |
||||
|
circles.value = [ |
||||
|
...shapeCircles, |
||||
|
{ |
||||
|
latitude: center1[1], |
||||
|
longitude: center1[0], |
||||
|
color: '#FF0000', |
||||
|
fillColor: '#FF0000', |
||||
|
radius: 5, |
||||
|
strokeWidth: 2 |
||||
|
}, |
||||
|
{ |
||||
|
latitude: center2[1], |
||||
|
longitude: center2[0], |
||||
|
color: '#FF0000', |
||||
|
fillColor: '#FF0000', |
||||
|
radius: 5, |
||||
|
strokeWidth: 2 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
// 设置地图视野 |
||||
|
// 设置地图视野,使用两个圆心作为边界点,并调整缩放级别 |
||||
|
mapContext.includePoints({ |
||||
|
points: [ |
||||
|
{ latitude: center1[1], longitude: center1[0] }, |
||||
|
{ latitude: center2[1], longitude: center2[0] } |
||||
|
], |
||||
|
padding: [50, 50, 110, 50], |
||||
|
success: (res) => { |
||||
|
setTimeout(() => { |
||||
|
// 调整偏移量使八字飞行路径保持垂直显示 |
||||
|
bearing.value = ((angle + 5) % 360 + 360) % 360; |
||||
|
}, 500); |
||||
|
// bearing.value = angle.toFixed(2) - 85; |
||||
|
console.log('地图视野设置成功', res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
console.error('地图视野设置失败', err); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 监听位置变化,更新八字飞行位置 |
||||
|
// watch(() => centerPoint.value, () => { |
||||
|
// if (!mapContext || !eightShaped) return; |
||||
|
// // 重新初始化八字飞行 |
||||
|
// initEightShaped(); |
||||
|
// }, { deep: true }); |
||||
|
|
||||
|
// 初始化轨迹回放 |
||||
|
function initDeviceCruise() { |
||||
|
console.log(mapContext); |
||||
|
|
||||
|
deviceCruise.setMap(mapContext); |
||||
|
deviceCruise.loadTrack(trajectory); |
||||
|
// markerPoints.value.push(deviceCruise._marker.value); |
||||
|
// console.log(markerPoints.value); |
||||
|
} |
||||
|
|
||||
|
// 计算属性:合并所有标记点 |
||||
|
// const allMarkers = computed(() => { |
||||
|
// // console.log(markerPoints.value, deviceCruise._marker.value); |
||||
|
// // eslint-disable-next-line vue/no-side-effects-in-computed-properties |
||||
|
// // markerPoints.value.push(deviceCruise._marker.value); |
||||
|
// |
||||
|
// // return [...markerPoints.value, deviceCruise._marker]; |
||||
|
// return markerPoints; |
||||
|
// }); |
||||
|
|
||||
|
// Taro.getLaunchOptionsSync().then(res => { |
||||
|
// console.log(res); |
||||
|
// }); |
||||
|
|
||||
|
// 组件卸载时清理资源 |
||||
|
onUnmounted(() => { |
||||
|
if (deviceCruise) { |
||||
|
deviceCruise.destroy(); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<view :class="s.root"> |
||||
|
<view class="mapBox"> |
||||
|
<map |
||||
|
id="map" |
||||
|
:longitude="centerPoint.lng" |
||||
|
:latitude="centerPoint.lat" |
||||
|
:polygons="polygons" |
||||
|
:circles="circles" |
||||
|
:markers="markerPoints" |
||||
|
:polyline="polyline" |
||||
|
:enable-satellite="true" |
||||
|
:enable-rotate="true" |
||||
|
:rotate="bearing" |
||||
|
/> |
||||
|
</view> |
||||
|
<TrackPlayback /> |
||||
|
<RealTimeData class="real-time-data" /> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
page { |
||||
|
height: 100%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.root { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 0 !important; |
||||
|
overflow: hidden; |
||||
|
// position: fixed; |
||||
|
// top: 0; |
||||
|
// left: 0; |
||||
|
|
||||
|
:global { |
||||
|
.mapBox { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
#map { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
// z-index: -1; |
||||
|
} |
||||
|
|
||||
|
.real-time-data { |
||||
|
//position: absolute; |
||||
|
//left: 20px; |
||||
|
//top: 20px; |
||||
|
//z-index: 2; |
||||
|
} |
||||
|
|
||||
|
.auth-tip { |
||||
|
position: absolute; |
||||
|
left: 40px; |
||||
|
right: 40px; |
||||
|
top: 40px; |
||||
|
background-color: fade(black, 50%); |
||||
|
backdrop-filter: blur(6px); |
||||
|
padding: 40px; |
||||
|
border-radius: 10px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
font-size: 28px; |
||||
|
color: yellow; |
||||
|
gap: 20px; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,358 @@ |
|||||
|
import * as turf from '@turf/turf'; |
||||
|
import deviceIcon from '../../assets/deviceIcon.png'; |
||||
|
|
||||
|
// 创建同心圆多边形点集
|
||||
|
function _createCircle({ center, redRadius, radiusDiff }) { |
||||
|
const outerRadius = redRadius + radiusDiff; // 外圆半径
|
||||
|
const innerRadius = redRadius - radiusDiff; // 内圆半径
|
||||
|
|
||||
|
// 配置圆形生成选项
|
||||
|
const options = {steps: 64, units: 'meters'}; // 增加steps以使圆形更平滑
|
||||
|
|
||||
|
// 生成外圆和内圆
|
||||
|
const outerCircle = turf.circle([center.lng, center.lat], outerRadius, options); |
||||
|
const innerCircle = turf.circle([center.lng, center.lat], innerRadius, options); |
||||
|
|
||||
|
// 生成红色参考圆
|
||||
|
const redCircle = turf.circle([center.lng, center.lat], redRadius, options); |
||||
|
|
||||
|
// 创建环形(外圆 - 内圆)
|
||||
|
const featureCollection = turf.featureCollection([outerCircle, innerCircle]); |
||||
|
const ring = turf.difference(featureCollection); |
||||
|
|
||||
|
// 转换坐标格式为小程序地图组件要求的格式
|
||||
|
const convertCoordinates = (coords) => { |
||||
|
return coords.map(coord => ({ |
||||
|
longitude: coord[0], |
||||
|
latitude: coord[1] |
||||
|
})); |
||||
|
}; |
||||
|
|
||||
|
// // 获取环形的坐标点集
|
||||
|
// const ringCoords = ring.geometry.coordinates[0];
|
||||
|
// 获取红色圆的坐标点集
|
||||
|
const redCircleCoords = redCircle.geometry.coordinates[0]; |
||||
|
|
||||
|
// 返回所需的点集
|
||||
|
return { |
||||
|
ring: [...convertCoordinates(ring.geometry.coordinates[0]), ...convertCoordinates(ring.geometry.coordinates[1])], |
||||
|
redCircle: convertCoordinates(redCircleCoords), |
||||
|
center: { |
||||
|
latitude: center.lat, |
||||
|
longitude: center.lng |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// 添加标记点
|
||||
|
export function addCircleMarkers(circle1, circle2, indexMap = [6, 0, 1, 3, 4, 5, 2], { radius = 5 }) { |
||||
|
const center1 = turf.point(circle1.center); |
||||
|
const center2 = turf.point(circle2.center); |
||||
|
|
||||
|
// 1. 计算两圆心连线方位角
|
||||
|
const bearing = turf.bearing(center1, center2); |
||||
|
|
||||
|
// 2. 生成基准交点
|
||||
|
const basePoint1 = turf.destination(center1, circle1.radius, bearing, { units: 'meters' }); |
||||
|
const basePoint2 = turf.destination(center2, circle2.radius, bearing + 180, { units: 'meters' }); |
||||
|
|
||||
|
// 3. 生成旋转点集合
|
||||
|
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); // 排除原始点
|
||||
|
}; |
||||
|
|
||||
|
// 4. 生成所有标记点
|
||||
|
const positions = [ |
||||
|
...generateRotatedPoints(circle1.center, basePoint1.geometry.coordinates), |
||||
|
...generateRotatedPoints(circle2.center, basePoint2.geometry.coordinates), |
||||
|
turf.midpoint(center1, center2).geometry.coordinates // 连线中点
|
||||
|
]; |
||||
|
|
||||
|
const circles = []; |
||||
|
const markers = []; |
||||
|
|
||||
|
// 5. 生成标记点数据
|
||||
|
positions.forEach((coord, index) => { |
||||
|
const markerPoint = { |
||||
|
longitude: coord[0], |
||||
|
latitude: coord[1] |
||||
|
}; |
||||
|
|
||||
|
// 为中点位置添加圆形背景
|
||||
|
if (index === 6) { |
||||
|
circles.push({ |
||||
|
latitude: markerPoint.latitude, |
||||
|
longitude: markerPoint.longitude, |
||||
|
// fillColor: '#00000000',
|
||||
|
color: '#FF0000', |
||||
|
radius, |
||||
|
strokeWidth: 2 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 添加数字标记
|
||||
|
markers.push({ |
||||
|
id: index, |
||||
|
latitude: markerPoint.latitude, |
||||
|
longitude: markerPoint.longitude, |
||||
|
iconPath: deviceIcon, |
||||
|
width: 10, |
||||
|
height: 10, |
||||
|
label: { |
||||
|
content: String(indexMap[index] + 1), |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 20, |
||||
|
textStrokeWidth: 2, |
||||
|
textStrokeColor: '#000000', |
||||
|
anchorX: -6, |
||||
|
anchorY: -12, |
||||
|
bgColor: '#00000000' |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
return { circles, markers }; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export function creatEightShaped(circleOptions1 = {}, circleOptions2 = {}, indexMap = [6, 0, 1, 3, 4, 5, 2], markerOptions = { radius: 50 }) { |
||||
|
const circle1 = _createCircle({ |
||||
|
center: { lng: circleOptions1.center[0], lat: circleOptions1.center[1] }, |
||||
|
redRadius: circleOptions1.radius, |
||||
|
radiusDiff: circleOptions1.radiusDiff |
||||
|
}); |
||||
|
const circle2 = _createCircle({ |
||||
|
center: { lng: circleOptions2.center[0], lat: circleOptions2.center[1] }, |
||||
|
redRadius: circleOptions2.radius, |
||||
|
radiusDiff: circleOptions2.radiusDiff |
||||
|
}); |
||||
|
const polygons = []; |
||||
|
|
||||
|
// 添加第一个圆的环形
|
||||
|
polygons.push({ |
||||
|
points: circle1.ring, |
||||
|
fillColor: '#FFFFFF80', |
||||
|
strokeColor: '#00000000', |
||||
|
strokeWidth: 2 |
||||
|
}); |
||||
|
|
||||
|
polygons.push({ |
||||
|
points: circle1.redCircle, |
||||
|
fillColor: '#00000000', |
||||
|
strokeColor: '#FF0000', |
||||
|
strokeWidth: 2 |
||||
|
}); |
||||
|
|
||||
|
// 添加第二个圆的环形
|
||||
|
polygons.push({ |
||||
|
points: circle2.ring, |
||||
|
fillColor: '#FFFFFF80', |
||||
|
strokeColor: '#00000000', |
||||
|
strokeWidth: 2 |
||||
|
}); |
||||
|
|
||||
|
polygons.push({ |
||||
|
points: circle2.redCircle, |
||||
|
fillColor: '#00000000', |
||||
|
strokeColor: '#FF0000', |
||||
|
strokeWidth: 2 |
||||
|
}); |
||||
|
|
||||
|
// 生成标记点和圆形标记
|
||||
|
const center1 = turf.point(circleOptions1.center); |
||||
|
const center2 = turf.point(circleOptions2.center); |
||||
|
|
||||
|
// 计算两圆心连线方位角
|
||||
|
const bearing = turf.bearing(center1, center2); |
||||
|
|
||||
|
// 生成基准交点
|
||||
|
const basePoint1 = turf.destination(center1, circleOptions1.radius, bearing, { units: 'meters' }); |
||||
|
const basePoint2 = turf.destination(center2, circleOptions2.radius, bearing + 180, { units: 'meters' }); |
||||
|
|
||||
|
// 生成旋转点集合
|
||||
|
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(circleOptions1.center, basePoint1.geometry.coordinates), |
||||
|
...generateRotatedPoints(circleOptions2.center, basePoint2.geometry.coordinates), |
||||
|
turf.midpoint(center1, center2).geometry.coordinates // 连线中点
|
||||
|
]; |
||||
|
|
||||
|
const circles = []; |
||||
|
const markers = []; |
||||
|
|
||||
|
// 生成标记点数据
|
||||
|
positions.forEach((coord, index) => { |
||||
|
const markerPoint = { |
||||
|
longitude: coord[0], |
||||
|
latitude: coord[1] |
||||
|
}; |
||||
|
|
||||
|
// 为中点位置添加圆形背景
|
||||
|
if (index === 6) { |
||||
|
circles.push({ |
||||
|
latitude: markerPoint.latitude, |
||||
|
longitude: markerPoint.longitude, |
||||
|
color: '#FF0000', |
||||
|
radius: markerOptions.radius, |
||||
|
strokeWidth: 2 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 添加数字标记
|
||||
|
markers.push({ |
||||
|
id: index, |
||||
|
latitude: markerPoint.latitude, |
||||
|
longitude: markerPoint.longitude, |
||||
|
iconPath: deviceIcon, |
||||
|
width: 1, |
||||
|
height: 1, |
||||
|
label: { |
||||
|
content: String(indexMap[index] + 1), |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 20, |
||||
|
textStrokeWidth: 2, |
||||
|
textStrokeColor: '#000000', |
||||
|
anchorX: -6, |
||||
|
anchorY: -12, |
||||
|
bgColor: '#00000000' |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
return { polygons, circles, markers }; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
// function fitView(mapContent) {
|
||||
|
// if (!this._circle1 || !this._circle2) return;
|
||||
|
//
|
||||
|
// const points = [
|
||||
|
// ...this._circle1.ring,
|
||||
|
// ...this._circle2.ring
|
||||
|
// ];
|
||||
|
//
|
||||
|
// // 计算所有点的边界
|
||||
|
// const lngs = points.map(p => p.longitude);
|
||||
|
// const lats = points.map(p => p.latitude);
|
||||
|
// const minLng = Math.min(...lngs);
|
||||
|
// const maxLng = Math.max(...lngs);
|
||||
|
// const minLat = Math.min(...lats);
|
||||
|
// const maxLat = Math.max(...lats);
|
||||
|
//
|
||||
|
// // 设置地图视野
|
||||
|
// this._map.includePoints({
|
||||
|
// points: [
|
||||
|
// { latitude: minLat, longitude: minLng },
|
||||
|
// { latitude: maxLat, longitude: maxLng }
|
||||
|
// ],
|
||||
|
// padding: [50]
|
||||
|
// });
|
||||
|
// }
|
||||
|
|
||||
|
// 创建八字飞行图形
|
||||
|
// export function EightShapedFlight() {
|
||||
|
// // 创建八字形
|
||||
|
//
|
||||
|
// // 调整视野以适应两个圆形
|
||||
|
//
|
||||
|
//
|
||||
|
// // 销毁实例
|
||||
|
// destroy() {
|
||||
|
// this._circle1 = null;
|
||||
|
// this._circle2 = null;
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
|
||||
|
///////////////
|
||||
|
|
||||
|
/** |
||||
|
* 将二维坐标数组转换为轨迹对象,包含自动生成的时间戳和航向角 |
||||
|
* @param {Array} coords 二维数组,格式如 [[lng, lat], ...] |
||||
|
* @param {Object} [options] 配置选项 |
||||
|
* @param {Date|string} [options.startTime] 起始时间(默认当前时间) |
||||
|
* @param {number} [options.interval=1000] 时间间隔(毫秒) |
||||
|
* @param {boolean} [options.calculateYaw=true] 是否计算航向角 |
||||
|
* @returns {Object} 轨迹对象 {id: string, points: Array} |
||||
|
*/ |
||||
|
export function convertToTrajectory(coords, options = {}) { |
||||
|
// 参数处理
|
||||
|
const { |
||||
|
startTime = new Date(), |
||||
|
interval = 1000, |
||||
|
calculateYaw = true |
||||
|
} = options; |
||||
|
|
||||
|
const startTimestamp = new Date(startTime).getTime(); |
||||
|
const id = `trajectory_${startTimestamp}`; |
||||
|
|
||||
|
// 核心转换逻辑
|
||||
|
const points = coords.map(([lng, lat], index, arr) => { |
||||
|
// 生成时间戳
|
||||
|
const timestamp = startTimestamp + index * interval; |
||||
|
|
||||
|
// 航向角计算
|
||||
|
let yaw = 0; |
||||
|
if (calculateYaw && index > 0) { |
||||
|
const prev = arr[index - 1]; |
||||
|
yaw = calculateBearing(prev, [lng, lat]); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
lng, |
||||
|
lat, |
||||
|
timestamp, |
||||
|
yaw: parseFloat(yaw.toFixed(2)), |
||||
|
alt: index * 2 // 示例高度值
|
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
// 闭环处理(首尾坐标相同时)
|
||||
|
// if (calculateYaw && points.length > 1) {
|
||||
|
// const first = points[0];
|
||||
|
// const last = points[points.length - 1];
|
||||
|
// if (first.lng === last.lng && first.lat === last.lat) {
|
||||
|
// last.yaw = calculateBearing(points[points.length - 2], first);
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
return { id, points }; |
||||
|
} |
||||
|
|
||||
|
// 航向角计算函数(单位:度)
|
||||
|
export function calculateBearing(prev, current) { |
||||
|
// console.log(prev, current);
|
||||
|
const [lng1, lat1] = prev; |
||||
|
const [lng2, lat2] = current; |
||||
|
|
||||
|
const dLon = (lng2 - lng1) * Math.PI / 180; |
||||
|
const y = Math.sin(dLon) * Math.cos(lat2 * Math.PI / 180); |
||||
|
const x = Math.cos(lat1 * Math.PI / 180) * Math.sin(lat2 * Math.PI / 180) - |
||||
|
Math.sin(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.cos(dLon); |
||||
|
|
||||
|
let angle = Math.atan2(y, x); |
||||
|
return ((angle * 180 / Math.PI) + 360) % 360; // 转换为0-360度
|
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '享飞低空培训', |
||||
|
enablePullDownRefresh: false, |
||||
|
disableScroll: false, |
||||
|
}) |
@ -0,0 +1,350 @@ |
|||||
|
<script setup> |
||||
|
// import { ref } from 'vue'; |
||||
|
import Taro from "@tarojs/taro"; |
||||
|
import TabBar from '../../components/TabBar.vue'; |
||||
|
// import { useTaskStore } from "../../stores"; |
||||
|
// import { storeToRefs } from "pinia"; |
||||
|
import { reactive } from "vue"; |
||||
|
import { RectRight } from '@nutui/icons-vue-taro' |
||||
|
import list from '../../assets/list.png'; |
||||
|
import flower from '../../assets/flower.png'; |
||||
|
|
||||
|
Taro.hideHomeButton(); |
||||
|
|
||||
|
// const { getTaskList } = useTaskStore(); |
||||
|
// const { taskList } = storeToRefs(useTaskStore()); |
||||
|
|
||||
|
// const taskProgressList = ref([]) |
||||
|
|
||||
|
// const { params } = Taro.useRouter(); |
||||
|
|
||||
|
// if (params?.navToRecord) { |
||||
|
// // setTimeout(() => { |
||||
|
// onNavTo(); |
||||
|
// // }, 3000) |
||||
|
// } |
||||
|
|
||||
|
// onUnmounted(() => { |
||||
|
// // taskList.value = []; |
||||
|
// }); |
||||
|
|
||||
|
function onNavToFlight() { |
||||
|
Taro.navigateTo({ |
||||
|
url: '/pages/flight/index', |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function onNavToRoutePlan() { |
||||
|
Taro.navigateTo({ |
||||
|
url: '/pages/routePlan/index', |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function onNavToTrip() { |
||||
|
Taro.navigateTo({ |
||||
|
url: '/pages/returnTrip/index', |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function onNavToEvaluation() { |
||||
|
Taro.navigateTo({ |
||||
|
url: '/pages/evaluation/index', |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div class="container" > |
||||
|
<div class="item record-box-1" @click="onNavToFlight"> |
||||
|
<div class="nav-box"> |
||||
|
<div class="text">实践飞行</div> |
||||
|
<div class="right"> |
||||
|
<RectRight /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<image :src="list" class="image" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="item record-box-2" @click="onNavToRoutePlan"> |
||||
|
<div class="nav-box"> |
||||
|
<div class="text">航线规划</div> |
||||
|
<div class="right"> |
||||
|
<RectRight /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<image :src="list" class="image" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="item record-box-3" @click="onNavToTrip"> |
||||
|
<div class="nav-box"> |
||||
|
<div class="text">应急返航</div> |
||||
|
<div class="right"> |
||||
|
<RectRight /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<image :src="list" class="image" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="item record-box-4" @click="onNavToEvaluation"> |
||||
|
<div class="nav-box"> |
||||
|
<div class="text">自我讲评</div> |
||||
|
<div class="right"> |
||||
|
<RectRight /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<image :src="list" class="image" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="tip-content"> |
||||
|
<div class="tip-center"> |
||||
|
<div class="round"> |
||||
|
<div class="round2"></div> |
||||
|
</div> |
||||
|
<div class="content"> |
||||
|
<div class="text">本程序可查看学习过程、实操记录。</div> |
||||
|
<div class="text">每天给自己一个评价,进步更快哦 ~。</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="tmp"></div> |
||||
|
</div> |
||||
|
|
||||
|
<image :src="flower" class="flower" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="tip-center" v-if="false"> |
||||
|
<div style="display: flex;"> |
||||
|
<div>1、</div><div style="flex: 1">本程序可查看学习过程、实操记录。</div> |
||||
|
</div> |
||||
|
<div style="display: flex;margin-top: 10px;"> |
||||
|
<div>2、</div><div style="flex: 1">每天给自己一个评价,进步更快哦 ~。</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="bottom-text"> |
||||
|
低空培训系统技术支持 |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<TabBar /> |
||||
|
<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: 0 40px; |
||||
|
box-sizing: border-box; |
||||
|
overflow: hidden; |
||||
|
background-color: #f2fbff; |
||||
|
} |
||||
|
|
||||
|
.root { |
||||
|
height: 100%; |
||||
|
|
||||
|
:global { |
||||
|
.container { |
||||
|
margin-top: 80px; |
||||
|
//margin-bottom: 40px; |
||||
|
//text-align: center; |
||||
|
//font-weight: bold; |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
gap: 40px; |
||||
|
//align-items: center; |
||||
|
//justify-content: center; |
||||
|
|
||||
|
.item { |
||||
|
flex: 1; |
||||
|
box-sizing: border-box; |
||||
|
//object-fit: cover; |
||||
|
//width: 300px; |
||||
|
//height: 360px; |
||||
|
height: 300px; |
||||
|
//border: 6px solid #000; |
||||
|
border-radius: 20px; |
||||
|
//display: flex; |
||||
|
//align-items: center; |
||||
|
//justify-content: center; |
||||
|
position: relative; |
||||
|
font-size: 40px; |
||||
|
color: white; |
||||
|
padding: 40px; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
//&:active { |
||||
|
// background-color: rgba(204, 204, 204, 0.4); |
||||
|
//} |
||||
|
|
||||
|
&.record-box-1 { |
||||
|
box-shadow: 5px 5px 10px 5px rgba(0, 140, 112, 0.3); |
||||
|
background: linear-gradient(218.31deg, rgb(0, 195, 156) 3.576%,rgb(0, 140, 112) 94.437%); |
||||
|
|
||||
|
.nut-icon { |
||||
|
color: rgb(0, 195, 156); |
||||
|
font-size: 20px; |
||||
|
//width: 10px; |
||||
|
//height: 10px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.record-box-2 { |
||||
|
box-shadow: 5px 5px 10px 5px rgba(0, 89, 195, 0.3); |
||||
|
background: linear-gradient(218.31deg, rgb(66, 152, 255) 3.576%,rgb(0, 89, 195) 94.437%); |
||||
|
|
||||
|
.nut-icon { |
||||
|
color: rgb(66, 152, 255); |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.record-box-3 { |
||||
|
box-shadow: 5px 5px 10px 5px rgba(195, 107, 0, 0.3); |
||||
|
background: linear-gradient(218.31deg, rgb(255, 167, 66) 3.576%, rgb(195, 117, 0) 94.437%); |
||||
|
|
||||
|
.nut-icon { |
||||
|
color: rgb(255, 173, 66); |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.record-box-4 { |
||||
|
box-shadow: 5px 5px 10px 5px rgba(0, 195, 159, 0.3); |
||||
|
background: linear-gradient(218.31deg, rgb(66, 233, 255) 3.576%, rgb(0, 189, 195) 94.437%); |
||||
|
|
||||
|
.nut-icon { |
||||
|
color: rgb(66, 227, 255); |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.nav-box { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
|
||||
|
.right { |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
background-color: white; |
||||
|
border-radius: 50%; |
||||
|
|
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.image { |
||||
|
position: absolute; |
||||
|
bottom: -20px; |
||||
|
right: -20px; |
||||
|
width: 190px; |
||||
|
height: 180px; |
||||
|
object-fit: cover; |
||||
|
//filter: grayscale(50%); |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tab-content { |
||||
|
padding: 40px 10px; |
||||
|
|
||||
|
.at-input { |
||||
|
margin-left: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
margin-top: 60px; |
||||
|
} |
||||
|
|
||||
|
.tip-content { |
||||
|
margin-top: 80px; |
||||
|
position: relative; |
||||
|
|
||||
|
.tip-center { |
||||
|
overflow: hidden; |
||||
|
//position: absolute; |
||||
|
//left: 0; |
||||
|
//right: 0; |
||||
|
//bottom: 740px; |
||||
|
//text-align: center; |
||||
|
position: relative; |
||||
|
border-radius: 20px; |
||||
|
box-shadow: 4px 4px 12px 0px rgba(255, 237, 189, 0.4); |
||||
|
background: linear-gradient(180.00deg, rgb(255, 242, 206),rgb(255, 235, 181) 100%); |
||||
|
color: rgb(204, 119, 0); |
||||
|
padding: 40px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
//justify-content: ; |
||||
|
//padding: 0 40px; |
||||
|
|
||||
|
.round { |
||||
|
position: absolute; |
||||
|
top: -100px; |
||||
|
left: -100px; |
||||
|
overflow: hidden; |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
border-radius: 50%; |
||||
|
background: rgb(252, 228, 166); |
||||
|
z-index: 0; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
z-index: 1; |
||||
|
|
||||
|
.text + .text { |
||||
|
margin-top: 30px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tmp { |
||||
|
width: 286px; |
||||
|
//height: 300px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.flower { |
||||
|
position: absolute; |
||||
|
bottom: -20px; |
||||
|
right: -20px; |
||||
|
width: 286px; |
||||
|
height: 320px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.bottom-text { |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 240px; |
||||
|
text-align: center; |
||||
|
font-size: 28px; |
||||
|
//color: #058a00; |
||||
|
//font-weight: bold; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|
@ -0,0 +1,7 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '登录', |
||||
|
// navigationStyle: "custom",
|
||||
|
// navigationBarTextStyle:'white',
|
||||
|
enablePullDownRefresh: false, |
||||
|
disableScroll: false, |
||||
|
}) |
@ -0,0 +1,279 @@ |
|||||
|
<script setup> |
||||
|
import { reactive, ref, watch } from 'vue'; |
||||
|
import Taro from "@tarojs/taro"; |
||||
|
import { useAuthStore } from '../../stores'; |
||||
|
import * as auth from '../../utils/auth'; |
||||
|
import { Loading } from '@nutui/icons-vue-taro' |
||||
|
import { storeToRefs } from 'pinia'; |
||||
|
|
||||
|
const { loginWithPassword, wechatAuth, updateStudentAvatar, updateMemberAvatar } = useAuthStore(); |
||||
|
const { userInfo, isStudent } = storeToRefs(useAuthStore()); |
||||
|
|
||||
|
const showLoginDialog = ref(false); |
||||
|
const pageLoading = ref(false); |
||||
|
// const loggedInLoading = ref(false); |
||||
|
const aUrl = ref(''); |
||||
|
|
||||
|
function updataAvatar() { |
||||
|
if (!userInfo.value?.id) return; |
||||
|
if (!aUrl.value) return; |
||||
|
if (isStudent.value) { |
||||
|
updateStudentAvatar({ avatar: aUrl.value, id: userInfo.value?.id }).then(() => { |
||||
|
userInfo.value.avatar = aUrl.value; |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}); |
||||
|
} else { |
||||
|
updateMemberAvatar({ avatar: aUrl.value, id: userInfo.value?.id }).then(() => { |
||||
|
userInfo.value.avatar = aUrl.value; |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function wxCodeLogin(loadRes = {}) { |
||||
|
Taro.login({ |
||||
|
success: (loginRes) => { |
||||
|
wechatAuth({ code: loginRes.code, sceneId: loadRes.scene }).then(() => { |
||||
|
if (auth.getToken()) { |
||||
|
updataAvatar(); |
||||
|
showLoginDialog.value = false; |
||||
|
Taro.reLaunch({ url: '/pages/home/index' }); |
||||
|
// openToast('success', `登录成功`); |
||||
|
return; |
||||
|
} |
||||
|
if (loadRes.scene && (`${loadRes.scene}`.length > 6)) { |
||||
|
showLoginDialog.value = true; |
||||
|
return; |
||||
|
} |
||||
|
if (!loadRes.scene && showLoginDialog.value) { |
||||
|
openToast('warn', '未查询到用户,请先在地面站绑定'); |
||||
|
// showLoginDialog.value = true; |
||||
|
return; |
||||
|
} |
||||
|
console.log('userInfo', userInfo.value); |
||||
|
}).catch(({ msg }) => { |
||||
|
// console.log('eee', err); |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
pageLoading.value = false; |
||||
|
}); |
||||
|
}, |
||||
|
fail: () => { |
||||
|
pageLoading.value = false; |
||||
|
openToast('warn', '未得到微信授权Code'); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
Taro.useLoad((loadRes) => { |
||||
|
pageLoading.value = true; |
||||
|
console.log('app load', loadRes); |
||||
|
wxCodeLogin(loadRes); |
||||
|
}); |
||||
|
|
||||
|
function onGetAvatar() { |
||||
|
Taro.getUserProfile({ |
||||
|
desc: '用于完善用户资料', |
||||
|
success: (userProfile) => { |
||||
|
const { userInfo: { avatarUrl = '' } = {} } = userProfile || {}; |
||||
|
console.log('avatarUrl', avatarUrl); |
||||
|
aUrl.value = avatarUrl; |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function onLoggedIn() { |
||||
|
pageLoading.value = true; |
||||
|
wxCodeLogin(); |
||||
|
onGetAvatar(); |
||||
|
} |
||||
|
|
||||
|
const loading = ref(false); |
||||
|
|
||||
|
const formRef = ref(); |
||||
|
const INIT_FORM_DATA = { |
||||
|
phone: '', |
||||
|
password: '', |
||||
|
}; |
||||
|
const formData = ref({ ...INIT_FORM_DATA }); |
||||
|
|
||||
|
const submit = () => { |
||||
|
onGetAvatar(); |
||||
|
formRef.value?.validate().then(({ valid }) => { |
||||
|
if (!valid) return; |
||||
|
loading.value = true; |
||||
|
loginWithPassword(formData.value).then(() => { |
||||
|
updataAvatar(); |
||||
|
Taro.reLaunch({ url: '/pages/own/index' }); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Promise 异步校验 |
||||
|
const asyncValidator = (val) => { |
||||
|
const phoneReg = /^400(-?)[0-9]{7}$|^1\d{10}$|^0[0-9]{2,3}-[0-9]{7,8}$/; |
||||
|
if (!val) { |
||||
|
return Promise.reject('请输入手机号') |
||||
|
} else if (!phoneReg.test(val)) { |
||||
|
return Promise.reject('手机号格式不正确') |
||||
|
} else { |
||||
|
return Promise.resolve(''); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const rules = ref({ |
||||
|
phone: [ |
||||
|
{ required: true, message: '请填写手机号' }, |
||||
|
{ validator: asyncValidator, message: '电话格式不正确' } |
||||
|
], |
||||
|
password: [ |
||||
|
{ required: true, message: '请填写密码' }, |
||||
|
], |
||||
|
}) |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div class="title">低空培训系统</div> |
||||
|
<nut-form |
||||
|
ref="formRef" |
||||
|
label-position="top" |
||||
|
star-position="left" |
||||
|
:model-value="formData" |
||||
|
:rules="rules"> |
||||
|
<nut-form-item label="手机号" prop="phone"> |
||||
|
<nut-input v-model="formData.phone" placeholder="请输入手机号" type="number" /> |
||||
|
</nut-form-item> |
||||
|
<nut-form-item label="密码" prop="password"> |
||||
|
<nut-input v-model="formData.password" placeholder="请输入密码" type="password" /> |
||||
|
</nut-form-item> |
||||
|
<nut-form-item> |
||||
|
<nut-button :loading="loading" type="info" size="large" @click="submit">登录</nut-button> |
||||
|
</nut-form-item> |
||||
|
</nut-form> |
||||
|
|
||||
|
<div class="bottom-end">低空培训系统技术支持</div> |
||||
|
</div> |
||||
|
|
||||
|
<nut-toast :msg="state.msg" v-model:visible="state.show" :type="state.type" :cover="state.cover" :duration="2000" /> |
||||
|
<nut-dialog |
||||
|
title="提示" |
||||
|
v-model:visible="showLoginDialog" |
||||
|
:close-on-click-overlay="false"> |
||||
|
<div style="text-align: center;font-size: 18px;color: black;">请先在地面站绑定</div> |
||||
|
<template #footer> |
||||
|
<nut-button type="info" @click="onLoggedIn" :loading="loggedInLoading">已绑定</nut-button> |
||||
|
</template> |
||||
|
</nut-dialog> |
||||
|
<nut-overlay v-model:visible="pageLoading" :overlay-class="s.overlay" :close-on-click-overlay="false"> |
||||
|
<div class="loading-wrapper"> |
||||
|
<Loading style="color: white;font-size: 34px;" /> |
||||
|
</div> |
||||
|
</nut-overlay> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
page { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
// padding: 20px; |
||||
|
// box-sizing: border-box; |
||||
|
background: linear-gradient(135deg, #1a237e 0%, #0d47a1 100%); |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.overlay { |
||||
|
:global { |
||||
|
.loading-wrapper { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
height: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.root { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
overflow: hidden; |
||||
|
max-width: 90%; |
||||
|
// padding: 40px 20px; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
// flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
|
||||
|
:global { |
||||
|
.title { |
||||
|
position: absolute; |
||||
|
top: 250px; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
font-size: 48px; |
||||
|
text-align: center; |
||||
|
color: white; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.nut-form { |
||||
|
// margin-top: 200px; |
||||
|
border-radius: 16px; |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
transform: translateY(-20%); |
||||
|
// margin-top: 200px; |
||||
|
|
||||
|
.nut-cell-group__wrap { |
||||
|
box-shadow: none; |
||||
|
} |
||||
|
|
||||
|
.nut-button { |
||||
|
// width: 100%; |
||||
|
border-radius: 16px; |
||||
|
// height: 50px; |
||||
|
// line-height: 50px; |
||||
|
// font-size: 18px; |
||||
|
// font-weight: bold; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.bottom-end { |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 60px; |
||||
|
text-align: center; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
font-size: 24px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|
@ -0,0 +1,3 @@ |
|||||
|
export default { |
||||
|
navigationBarTitleText: '我的' |
||||
|
} |
@ -0,0 +1,171 @@ |
|||||
|
<script setup> |
||||
|
import TabBar from "../../components/TabBar.vue"; |
||||
|
import { ref } from "vue"; |
||||
|
import Taro from "@tarojs/taro"; |
||||
|
import { storeToRefs } from "pinia"; |
||||
|
import { useAuthStore } from "../../stores"; |
||||
|
import * as UserInfo from "../../utils/userInfo"; |
||||
|
import * as Auth from "../../utils/auth"; |
||||
|
import FormData from '../../core/FormData'; |
||||
|
|
||||
|
Taro.hideHomeButton(); |
||||
|
|
||||
|
const { userInfo, isStudent, isTeacher } = storeToRefs(useAuthStore()); |
||||
|
const { updateStudentAvatar, updateMemberAvatar, uploadFile } = useAuthStore(); |
||||
|
|
||||
|
const isOpened = ref(false); |
||||
|
const avatarUrl = ref(''); |
||||
|
|
||||
|
const weightClasses = { |
||||
|
small: "小型", |
||||
|
medium: "中型", |
||||
|
large: "大型", |
||||
|
} |
||||
|
|
||||
|
function handleConfirm() { |
||||
|
UserInfo.remove(); |
||||
|
Auth.removeToken(); |
||||
|
userInfo.value = {}; |
||||
|
Taro.reLaunch({ url: '/pages/login/index' }) |
||||
|
} |
||||
|
|
||||
|
const loading = ref(false); |
||||
|
function onChooseAvatar(e) { |
||||
|
loading.value = true; |
||||
|
const { avatarUrl: url = '' } = e.detail || {}; |
||||
|
// console.log('avatarUrl', e); |
||||
|
const form = new FormData(); |
||||
|
form.appendFile('file', url); |
||||
|
form.append('type', 'avatar'); |
||||
|
const fileData = form.getData(); |
||||
|
uploadFile(fileData.buffer, { header: { 'content-type': `${fileData.contentType}` } }).then(({ data }) => { |
||||
|
avatarUrl.value = data.fileUrl; |
||||
|
form.clearCacheData(); |
||||
|
if (isStudent.value) { |
||||
|
console.log('isS'); |
||||
|
updateStudentAvatar({ avatar: avatarUrl.value, id: userInfo.value?.id }).then(() => { |
||||
|
userInfo.value.avatar = avatarUrl.value; |
||||
|
Taro.showToast({ title: '头像更新成功', icon: 'success' }); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) Taro.showToast({ title: msg, icon: 'error' }); |
||||
|
}).finally(() => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
} else { |
||||
|
console.log('isM'); |
||||
|
updateMemberAvatar({ avatar: avatarUrl.value, id: userInfo.value?.id }).then(() => { |
||||
|
userInfo.value.avatar = avatarUrl.value; |
||||
|
// showAvatarDialog.value = false; |
||||
|
Taro.showToast({ title: '头像更新成功', icon: 'success' }); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) Taro.showToast({ title: msg, icon: 'error' }); |
||||
|
}).finally(() => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
} |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) Taro.showToast({ title: msg, icon: 'error' }); |
||||
|
}); |
||||
|
|
||||
|
// avatarUrl.value = url; |
||||
|
// if (!avatarUrl.value) { |
||||
|
// loading.value = false; |
||||
|
// return; |
||||
|
// } |
||||
|
} |
||||
|
|
||||
|
function onCancel(e) { |
||||
|
console.log('aaa', e); |
||||
|
} |
||||
|
|
||||
|
Taro.onError((e) => { |
||||
|
console.log('onError', e); |
||||
|
}) |
||||
|
|
||||
|
Taro.useError((e) => { |
||||
|
console.log('useError', e); |
||||
|
}) |
||||
|
|
||||
|
Taro.useDidShow((e) => { |
||||
|
console.log('useDidShow', e); |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div class="top"> |
||||
|
<nut-avatar size="large"><img :src="userInfo?.avatar" /></nut-avatar> |
||||
|
<div class="name">{{ userInfo?.name || '-' }}</div> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="isStudent"> |
||||
|
<nut-cell title="班级" :desc="userInfo?.class?.name || '-'"></nut-cell> |
||||
|
<nut-cell title="考试等级" :desc="userInfo?.licenseGrade?.name || '-'"></nut-cell> |
||||
|
<nut-cell title="无人机重量级" :desc="weightClasses[userInfo?.weightClass] || '-'"></nut-cell> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else> |
||||
|
<nut-cell v-if="userInfo?.courses" title="教学内容" :desc="userInfo?.courses || '-'"></nut-cell> |
||||
|
<nut-cell v-if="userInfo?.class" title="负责班级" :desc="userInfo?.class?.name || '-'"></nut-cell> |
||||
|
<nut-cell v-if="userInfo?.organization" title="机构名称" :desc="userInfo?.organization?.name || '-'"></nut-cell> |
||||
|
</div> |
||||
|
|
||||
|
<nut-button shape="square" class="btn" type="info" size="large" open-type="chooseAvatar" @chooseavatar="onChooseAvatar" :loading="loading" @error="onCancel">更换头像</nut-button> |
||||
|
<nut-button shape="square" type="primary" v-if="!isStudent && !isTeacher" class="btn" size="large" @click="isOpened = true">退出</nut-button> |
||||
|
</div> |
||||
|
|
||||
|
<nut-dialog |
||||
|
title="退出提示" |
||||
|
content="确定退出当前账号吗?" |
||||
|
v-model:visible="isOpened" |
||||
|
@ok="handleConfirm" |
||||
|
/> |
||||
|
|
||||
|
<TabBar /> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
page { |
||||
|
box-sizing: content-box; |
||||
|
overflow: hidden; |
||||
|
background-color: #f2fbff !important; |
||||
|
// background-color: #f2fbff; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.root { |
||||
|
box-sizing: content-box; |
||||
|
background-color: white; |
||||
|
margin: 30px; |
||||
|
height: 100%; |
||||
|
padding: 30px; |
||||
|
border-radius: 30px; |
||||
|
|
||||
|
:global { |
||||
|
.top { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 30px; |
||||
|
|
||||
|
.name { |
||||
|
margin-right: 50px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//.at-list__item-extra { |
||||
|
// max-width: 380px; |
||||
|
// |
||||
|
// .item-extra__info { |
||||
|
// white-space: unset; |
||||
|
// } |
||||
|
//} |
||||
|
|
||||
|
.btn { |
||||
|
margin-top: 40px; |
||||
|
border-radius: 16px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,6 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '应急返航', |
||||
|
disableSwipeBack: true, |
||||
|
enablePullDownRefresh: true, |
||||
|
backgroundTextStyle: 'dark', |
||||
|
}) |
@ -0,0 +1,215 @@ |
|||||
|
<script setup> |
||||
|
import { reactive, ref } from 'vue'; |
||||
|
import Taro from "@tarojs/taro"; |
||||
|
import { storeToRefs } from "pinia"; |
||||
|
import { useReturnTripStore } from "../../stores"; |
||||
|
|
||||
|
const { getReturnTripList } = useReturnTripStore(); |
||||
|
const { returnTripList, returnTripExtra, returnTripQueries } = storeToRefs(useReturnTripStore()); |
||||
|
|
||||
|
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; |
||||
|
returnTripQueries.value.pageNum = 1; |
||||
|
getReturnTripList().then(() => { |
||||
|
Taro.stopPullDownRefresh(); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
isRefreshing.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onLoadMore() { |
||||
|
if (loading.value) return; |
||||
|
if (returnTripList.value.length >= returnTripExtra.value.total) return; |
||||
|
loading.value = true; |
||||
|
returnTripQueries.value.pageNum += 1; |
||||
|
getReturnTripList().catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onNavTo(id) { |
||||
|
Taro.navigateTo({ |
||||
|
url: `/pages/returnTripMap/index?id=${id}`, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Taro.usePullDownRefresh(() => { |
||||
|
onRefresh(); |
||||
|
}); |
||||
|
|
||||
|
Taro.useReachBottom(() => { |
||||
|
onLoadMore(); |
||||
|
}); |
||||
|
|
||||
|
Taro.useDidShow(() => { |
||||
|
getReturnTripList().catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div class="list"> |
||||
|
<div class="item" v-for="item in returnTripList" :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.startTime || '-' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span>结束时间:{{ item.endTime || '-' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span><nut-tag :type="item.isPass ? 'success' : 'danger'">{{ item.isPass ? '通过' : '未通过' }}</nut-tag></span> |
||||
|
<span><nut-tag type="warning" v-if="item.inProgress">进行中</nut-tag></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<nut-empty v-if="!loading && !returnTripList.length" description="暂无数据" /> |
||||
|
<nut-infiniteloading |
||||
|
v-if="returnTripList.length" |
||||
|
load-txt="加载中..." |
||||
|
load-more-txt="没有更多了" |
||||
|
:has-more="returnTripList.length < returnTripExtra.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: hidden; |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
|
||||
|
.class-name { |
||||
|
font-size: 32px; |
||||
|
color: #666; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.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,5 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '应急返航回放', |
||||
|
navigationStyle: 'default', |
||||
|
pageOrientation: 'landscape' |
||||
|
}) |
@ -0,0 +1,116 @@ |
|||||
|
<script setup> |
||||
|
import { onMounted, ref, onUnmounted } from "vue"; |
||||
|
import * as Taro from "@tarojs/taro"; |
||||
|
import TrackPlayback from '../../components/TrackPlayback.vue'; |
||||
|
import RealTimeData from '../../components/RealTimeData.vue'; |
||||
|
import { initReturnTripConfig } from './utils'; |
||||
|
|
||||
|
// 地图中心点位置(Home点) |
||||
|
const centerPoint = ref({ |
||||
|
lat: 26.647661, |
||||
|
lng: 106.630154 |
||||
|
}); |
||||
|
|
||||
|
// 地图上下文 |
||||
|
let mapContext = null; |
||||
|
|
||||
|
// 多边形配置 |
||||
|
const polygons = ref([]); |
||||
|
// 标记点配置 |
||||
|
const markerPoints = ref([]); |
||||
|
// 飞行轨迹配置 |
||||
|
const polyline = ref([]); |
||||
|
|
||||
|
// 初始化地图 |
||||
|
onMounted(() => { |
||||
|
mapContext = Taro.createMapContext('map'); |
||||
|
initReturnTrip(); |
||||
|
}); |
||||
|
|
||||
|
// 初始化返航界面 |
||||
|
function initReturnTrip() { |
||||
|
if (!mapContext) return; |
||||
|
// 获取返航界面配置 |
||||
|
const config = initReturnTripConfig(centerPoint.value); |
||||
|
markerPoints.value = config.markers; |
||||
|
polygons.value = config.polygons; |
||||
|
|
||||
|
// 设置地图视野 |
||||
|
mapContext.includePoints({ |
||||
|
points: [{ |
||||
|
longitude: centerPoint.value.lng, |
||||
|
latitude: centerPoint.value.lat, |
||||
|
}], |
||||
|
padding: [50, 50, 110, 50], |
||||
|
success: (res) => { |
||||
|
console.log('地图视野设置成功', res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
console.error('地图视野设置失败', err); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// console.log(markers); |
||||
|
} |
||||
|
|
||||
|
// 组件卸载时清理资源 |
||||
|
onUnmounted(() => { |
||||
|
// deviceCruise.destroy(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<view :class="s.root"> |
||||
|
<view class="mapBox"> |
||||
|
<map |
||||
|
id="map" |
||||
|
:longitude="centerPoint.lng" |
||||
|
:latitude="centerPoint.lat" |
||||
|
:polygons="polygons" |
||||
|
:markers="markerPoints" |
||||
|
:polyline="polyline" |
||||
|
:enable-satellite="true" |
||||
|
/> |
||||
|
</view> |
||||
|
<TrackPlayback /> |
||||
|
<RealTimeData class="real-time-data" /> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
page { |
||||
|
height: 100%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.root { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 0 !important; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
:global { |
||||
|
.mapBox { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
#map { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.real-time-data { |
||||
|
//position: absolute; |
||||
|
//left: 20px; |
||||
|
//top: 20px; |
||||
|
//z-index: 2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,96 @@ |
|||||
|
import * as turf from '@turf/turf'; |
||||
|
import homeBg from '../../assets/home-marker.png'; |
||||
|
import Taro from "@tarojs/taro"; |
||||
|
|
||||
|
const deviceInfo = Taro.getDeviceInfo() |
||||
|
|
||||
|
// 创建同心圆多边形点集
|
||||
|
export function createCircle({ center, innerRadius, outerRadius }) { |
||||
|
// 配置圆形生成选项
|
||||
|
const options = { steps: 64, units: 'meters' }; // 增加steps以使圆形更平滑
|
||||
|
|
||||
|
// 生成外圆和内圆
|
||||
|
const outerCircle = turf.circle([center.lng, center.lat], outerRadius, options); |
||||
|
const innerCircle = turf.circle([center.lng, center.lat], innerRadius, options); |
||||
|
|
||||
|
// 创建环形(外圆 - 内圆)
|
||||
|
const featureCollection = turf.featureCollection([outerCircle, innerCircle]); |
||||
|
const ring = turf.difference(featureCollection); |
||||
|
|
||||
|
// 转换坐标格式为小程序地图组件要求的格式
|
||||
|
const convertCoordinates = (coords) => { |
||||
|
return coords.map(coord => ({ |
||||
|
longitude: coord[0], |
||||
|
latitude: coord[1] |
||||
|
})); |
||||
|
}; |
||||
|
|
||||
|
// 获取环形的坐标点集
|
||||
|
const ringCoords = ring.geometry.coordinates; |
||||
|
|
||||
|
// 返回所需的点集
|
||||
|
return { |
||||
|
ring: [...convertCoordinates(ringCoords[0]), ...convertCoordinates(ringCoords[1])], |
||||
|
center: { |
||||
|
latitude: center.lat, |
||||
|
longitude: center.lng |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// 初始化返航界面配置
|
||||
|
export function initReturnTripConfig(centerPoint, innerRadius = 50, outerRadius = 100) { |
||||
|
const markerOptions = { |
||||
|
...(`${deviceInfo?.system || ''}`.includes('iOS') ? { |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 12, |
||||
|
// anchorX: -5,
|
||||
|
anchorY: -16, |
||||
|
textAlign: 'center', |
||||
|
} : { |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 12, |
||||
|
anchorX: -4, |
||||
|
anchorY: -9, |
||||
|
}) |
||||
|
} |
||||
|
const homePoint = [centerPoint.lng, centerPoint.lat]; |
||||
|
|
||||
|
// 添加Home点标记
|
||||
|
const homeMarker = [{ |
||||
|
id: 1, |
||||
|
longitude: homePoint[0], |
||||
|
latitude: homePoint[1], |
||||
|
iconPath: homeBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
label: { |
||||
|
...markerOptions, |
||||
|
content: 'H' |
||||
|
} |
||||
|
}]; |
||||
|
|
||||
|
// 生成同心圆点集
|
||||
|
const circlePoints = createCircle({ |
||||
|
center: centerPoint, |
||||
|
innerRadius, // 内圆半径(米)
|
||||
|
outerRadius // 外圆半径(米)
|
||||
|
}); |
||||
|
|
||||
|
// 设置多边形配置
|
||||
|
const polygons = [ |
||||
|
{ |
||||
|
points: circlePoints.ring, |
||||
|
fillColor: '#FFFFFF80', |
||||
|
strokeColor: '#00000000', |
||||
|
strokeWidth: 2 |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
return { |
||||
|
markers: homeMarker, |
||||
|
polygons, |
||||
|
// homePoint: { latitude: homePoint[1], longitude: homePoint[0] }
|
||||
|
}; |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '航线规划', |
||||
|
disableSwipeBack: true, |
||||
|
enablePullDownRefresh: true, |
||||
|
backgroundTextStyle: 'dark', |
||||
|
}) |
@ -0,0 +1,221 @@ |
|||||
|
<script setup> |
||||
|
import { reactive, ref } from 'vue'; |
||||
|
import Taro from "@tarojs/taro"; |
||||
|
import { storeToRefs } from "pinia"; |
||||
|
import { useRoutePlanStore } from "../../stores"; |
||||
|
|
||||
|
const { getRoutePlanList } = useRoutePlanStore(); |
||||
|
const { routePlanList, routePlanExtra, routePlanQueries } = storeToRefs(useRoutePlanStore()); |
||||
|
|
||||
|
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; |
||||
|
routePlanQueries.value.pageNum = 1; |
||||
|
getRoutePlanList().then(() => { |
||||
|
Taro.stopPullDownRefresh(); |
||||
|
}).catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
isRefreshing.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onLoadMore() { |
||||
|
if (loading.value) return; |
||||
|
if (routePlanList.value.length >= routePlanExtra.value.total) return; |
||||
|
loading.value = true; |
||||
|
routePlanQueries.value.pageNum += 1; |
||||
|
getRoutePlanList().catch(({ msg }) => { |
||||
|
if (msg) openToast('warn', msg); |
||||
|
}).finally(() => { |
||||
|
loading.value = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onNavTo(id) { |
||||
|
Taro.navigateTo({ |
||||
|
url: `/pages/routePlanMap/index?id=${id}`, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Taro.usePullDownRefresh(() => { |
||||
|
onRefresh(); |
||||
|
}); |
||||
|
|
||||
|
Taro.useReachBottom(() => { |
||||
|
onLoadMore(); |
||||
|
}); |
||||
|
|
||||
|
Taro.useDidShow(() => { |
||||
|
getRoutePlanList().catch(err => { |
||||
|
openToast('warn', err?.description); |
||||
|
}); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div class="list"> |
||||
|
<div class="item" v-for="item in routePlanList" :key="item.answerId" :class="{ 'is-pass': item.isPass }"> |
||||
|
<div class="title"> |
||||
|
<span>{{ item.studentName || '-' }}</span> |
||||
|
<nut-button size="mini" type="info" @click="onNavTo(item.answerId)">查看</nut-button> |
||||
|
</div> |
||||
|
<div class="info"> |
||||
|
<div class="info-row"> |
||||
|
<span>题目:{{ item.questionName || '-' }}</span> |
||||
|
<span>模板:{{ item.templateName || '-' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span>教师:{{ item.teacherName || '-' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span>开始时间:{{ item.beginTime || '-' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span>结束时间:{{ item.finishTime || '-' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span><nut-tag :type="item.isPass ? 'success' : 'danger'">{{ item.isPass ? '通过' : '未通过' }}</nut-tag></span> |
||||
|
</div> |
||||
|
<div v-if="item.errorMsg" class="error-msg">{{ item.errorMsg }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<nut-empty v-if="!loading && !routePlanList.length" description="暂无数据" /> |
||||
|
<nut-infiniteloading |
||||
|
v-if="routePlanList.length" |
||||
|
load-txt="加载中..." |
||||
|
load-more-txt="没有更多了" |
||||
|
:has-more="routePlanList.length < routePlanExtra.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: hidden; |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
|
||||
|
.question-name { |
||||
|
font-size: 32px; |
||||
|
color: #666; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.error-msg { |
||||
|
margin-top: 16px; |
||||
|
color: #ff4d4f; |
||||
|
font-size: 28px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,5 @@ |
|||||
|
export default { |
||||
|
navigationBarTitleText: '航线规划', |
||||
|
navigationStyle: 'default', |
||||
|
pageOrientation: 'landscape' |
||||
|
} |
@ -0,0 +1,433 @@ |
|||||
|
<script setup> |
||||
|
import { onMounted, ref, computed } from "vue"; |
||||
|
import RoutePointInfo from '../../components/RoutePointInfo.vue'; |
||||
|
import ExamResult from '../../components/ExamResult.vue'; |
||||
|
import * as Taro from "@tarojs/taro"; |
||||
|
// import * as turf from '@turf/turf'; |
||||
|
import pointBg from '../../assets/point-marker.png'; |
||||
|
import homeBg from '../../assets/home-marker.png'; |
||||
|
import tmpBg from '../../assets/transparent-marker.png'; |
||||
|
import { Close, Horizontal } from '@nutui/icons-vue-taro'; |
||||
|
|
||||
|
const deviceInfo = Taro.getDeviceInfo() |
||||
|
|
||||
|
// 考题显示状态 |
||||
|
const isExamVisible = ref(false); |
||||
|
const isExamExpanded = ref(false); |
||||
|
|
||||
|
// 切换考题显示状态 |
||||
|
const toggleExam = () => { |
||||
|
isExamVisible.value = !isExamVisible.value; |
||||
|
}; |
||||
|
|
||||
|
// 在考题显示后添加展开效果 |
||||
|
// watch(() => isExamVisible.value, (newVal) => { |
||||
|
// if (newVal) { |
||||
|
// setTimeout(() => { |
||||
|
// isExamExpanded.value = true; |
||||
|
// }, 50); |
||||
|
// } else { |
||||
|
// isExamExpanded.value = false; |
||||
|
// } |
||||
|
// }); |
||||
|
|
||||
|
// 模拟考题数据 |
||||
|
const questionData = ref({ |
||||
|
imageUrl: 'https://example.com/exam-image.jpg', |
||||
|
content: `航线要求: |
||||
|
(一) 起飞点(返航点)与考试点位的相对方位由委任代表根据现场环境等情况进行决定。于起飞前规划一个等边三角形并循环执行,边长为a,航线相对地面高度为b,水平速度为c,垂直速度为d,转弯方式为停止转弯,停留时间不作要求。 |
||||
|
|
||||
|
(二) a 值建议为30米,b 值建议为30米,c 值建议为3m/s,d 值 建议为1m/s,(航线方位及各数值可由委任代表按实际情况进行调整,考题以委任代表规定数值为准)。 |
||||
|
|
||||
|
(三) 题中心为起飞点A,地面中任取,须在地图中标明,1点在A的45°,30米方位,1到2的顺向角为135°。` |
||||
|
}); |
||||
|
|
||||
|
const markerOption = computed(() => { |
||||
|
if (`${deviceInfo?.system || ''}`.includes('iOS')) { |
||||
|
return { |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 12, |
||||
|
// anchorX: -5, |
||||
|
anchorY: -16, |
||||
|
textAlign: 'center', |
||||
|
} |
||||
|
} |
||||
|
return { |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 12, |
||||
|
anchorX: -4, |
||||
|
anchorY: -9, |
||||
|
// textAlign: 'center', |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
console.log(deviceInfo.system) |
||||
|
|
||||
|
Taro.hideHomeButton(); |
||||
|
|
||||
|
// 地图中心点位置 |
||||
|
const centerPoint = ref({ |
||||
|
lat: 39.908692, |
||||
|
lng: 116.397477 |
||||
|
}); |
||||
|
|
||||
|
// 地图上下文 |
||||
|
let mapContext = null; |
||||
|
|
||||
|
// 是否闭合航线 |
||||
|
const isClosedRoute = ref(true); |
||||
|
|
||||
|
// home点位置(起始点) |
||||
|
const homePoint = ref({ |
||||
|
latitude: 39.908692, |
||||
|
longitude: 116.397477 |
||||
|
}); |
||||
|
|
||||
|
// 航线点位数组 |
||||
|
const routePoints = ref([{ |
||||
|
latitude: 39.909192, // 点1 |
||||
|
longitude: 116.398277 |
||||
|
}, { |
||||
|
latitude: 39.909892, // 点4 |
||||
|
longitude: 116.398977 |
||||
|
}, { |
||||
|
latitude: 39.909392, // 点2 |
||||
|
longitude: 116.399477 |
||||
|
}, { |
||||
|
latitude: 39.908892, // 点3 |
||||
|
longitude: 116.399977 |
||||
|
}]); |
||||
|
|
||||
|
// 计算两点之间的距离(单位:米) |
||||
|
const calculateDistance = (point1, point2) => { |
||||
|
const R = 6371e3; // 地球半径(米) |
||||
|
const φ1 = point1.latitude * Math.PI/180; |
||||
|
const φ2 = point2.latitude * Math.PI/180; |
||||
|
const Δφ = (point2.latitude - point1.latitude) * Math.PI/180; |
||||
|
const Δλ = (point2.longitude - point1.longitude) * Math.PI/180; |
||||
|
|
||||
|
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + |
||||
|
Math.cos(φ1) * Math.cos(φ2) * |
||||
|
Math.sin(Δλ/2) * Math.sin(Δλ/2); |
||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); |
||||
|
|
||||
|
// 计算中点坐标 |
||||
|
const midLongitude = (point1.longitude + point2.longitude) / 2; |
||||
|
const midLatitude = (point1.latitude + point2.latitude) / 2; |
||||
|
|
||||
|
return { mid: [midLongitude, midLatitude], distance: Math.round(R * c) }; |
||||
|
}; |
||||
|
|
||||
|
// 标记点配置 |
||||
|
const markers = ref([ |
||||
|
{ |
||||
|
id: 89, |
||||
|
latitude: homePoint.value.latitude, |
||||
|
longitude: homePoint.value.longitude, |
||||
|
iconPath: homeBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
label: { |
||||
|
content: 'H', |
||||
|
// color: '#FFFFFF', |
||||
|
...markerOption.value, |
||||
|
// textAlign: 'center', |
||||
|
borderWidth: 1, |
||||
|
borderColor: '#000', |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 1, |
||||
|
latitude: routePoints.value[0].latitude, |
||||
|
longitude: routePoints.value[0].longitude, |
||||
|
iconPath: pointBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
label: { |
||||
|
content: '1', |
||||
|
...markerOption.value, |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 2, |
||||
|
latitude: routePoints.value[1].latitude, |
||||
|
longitude: routePoints.value[1].longitude, |
||||
|
iconPath: pointBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
label: { |
||||
|
content: '2', |
||||
|
...markerOption.value, |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 3, |
||||
|
latitude: routePoints.value[2].latitude, |
||||
|
longitude: routePoints.value[2].longitude, |
||||
|
iconPath: pointBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
label: { |
||||
|
content: '3', |
||||
|
...markerOption.value, |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 4, |
||||
|
latitude: routePoints.value[3].latitude, |
||||
|
longitude: routePoints.value[3].longitude, |
||||
|
iconPath: pointBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
label: { |
||||
|
content: '4', |
||||
|
...markerOption.value, |
||||
|
} |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
// 航线配置 |
||||
|
const polyline = computed(() => { |
||||
|
const points = [homePoint.value, ...routePoints.value]; |
||||
|
if (isClosedRoute.value) { |
||||
|
points.push({ |
||||
|
latitude: routePoints.value[0].latitude, |
||||
|
longitude: routePoints.value[0].longitude |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 计算每段航线的中点位置和距离 |
||||
|
const segments = []; |
||||
|
for (let i = 0; i < points.length - 1; i++) { |
||||
|
const tmp = calculateDistance(points[i], points[i + 1]); |
||||
|
const midPoint = { |
||||
|
latitude: tmp.mid[1], |
||||
|
longitude: tmp.mid[0], |
||||
|
}; |
||||
|
|
||||
|
// 添加距离标注marker |
||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties |
||||
|
markers.value.push({ |
||||
|
id: 1000 + i, |
||||
|
iconPath: tmpBg, |
||||
|
width: 20, |
||||
|
height: 20, |
||||
|
anchor: { x: 0.5, y: 0.5 }, |
||||
|
latitude: midPoint.latitude, |
||||
|
longitude: midPoint.longitude, |
||||
|
// content: `${distance}m` |
||||
|
label: { |
||||
|
content: `${tmp.distance}m`, |
||||
|
color: '#FFFFFF', |
||||
|
fontSize: 12, |
||||
|
textAlign: 'center', |
||||
|
anchorY: -9, |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return [{ |
||||
|
points, |
||||
|
color: '#1989fa', |
||||
|
width: 4, |
||||
|
}]; |
||||
|
}); |
||||
|
|
||||
|
// 初始化地图 |
||||
|
onMounted(() => { |
||||
|
mapContext = Taro.createMapContext('map'); |
||||
|
|
||||
|
// 设置地图视野 |
||||
|
mapContext.includePoints({ |
||||
|
points: routePoints.value, |
||||
|
padding: [50, 50, 110, 50], |
||||
|
success: (res) => { |
||||
|
console.log('地图视野设置成功', res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
console.error('地图视野设置失败', err); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
console.log(markers); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<view :class="s.root"> |
||||
|
<view class="mapBox"> |
||||
|
<map |
||||
|
id="map" |
||||
|
:longitude="centerPoint.lng" |
||||
|
:latitude="centerPoint.lat" |
||||
|
:markers="markers" |
||||
|
:polyline="polyline" |
||||
|
:enable-satellite="true" |
||||
|
/> |
||||
|
</view> |
||||
|
<RoutePointInfo :pointInfo="[]" /> |
||||
|
<ExamResult :is-passed="false" /> |
||||
|
<div :class="[s.examQuestion, { 'is-expanded': isExamVisible }]" v-show="isExamVisible"> |
||||
|
<div class="title"> |
||||
|
<div class="title-content"> |
||||
|
<Horizontal /> |
||||
|
<span>考题</span> |
||||
|
</div> |
||||
|
<Close class="close-btn" @click="toggleExam" /> |
||||
|
</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> |
||||
|
<div :class="s.showExamBtn" @click="toggleExam" v-show="!isExamVisible"> |
||||
|
<Horizontal /> |
||||
|
<span>显示考题</span> |
||||
|
</div> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
page { |
||||
|
height: 100%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.root { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 0 !important; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
:global { |
||||
|
.mapBox { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
#map { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.examQuestion { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
width: 50%; |
||||
|
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; |
||||
|
|
||||
|
&:global { |
||||
|
&.is-expanded { |
||||
|
transform: translateX(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:global { |
||||
|
.title { |
||||
|
padding: 5px; |
||||
|
font-size: 12px; |
||||
|
font-weight: bold; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
|
||||
|
.title-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 2px; |
||||
|
|
||||
|
.nut-icon { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.close-btn { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
cursor: pointer; |
||||
|
padding: 2px; |
||||
|
font-size: 12px; |
||||
|
&:hover { |
||||
|
opacity: 0.8; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
flex: 1; |
||||
|
padding: 12px; |
||||
|
overflow-y: auto; |
||||
|
|
||||
|
.question-image { |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
img { |
||||
|
width: 100%; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.requirements { |
||||
|
font-size: 12px; |
||||
|
line-height: 1.5; |
||||
|
white-space: pre-wrap; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.showExamBtn { |
||||
|
position: fixed; |
||||
|
right: 16px; |
||||
|
bottom: 16px; |
||||
|
background: rgba(0, 0, 0, 0.8); |
||||
|
color: #fff; |
||||
|
padding: 8px 16px; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
z-index: 100; |
||||
|
font-size: 14px; |
||||
|
|
||||
|
&:hover { |
||||
|
background: rgba(0, 0, 0, 0.9); |
||||
|
} |
||||
|
|
||||
|
:global { |
||||
|
.nut-icon { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,5 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '场地监管', |
||||
|
enablePullDownRefresh: false, |
||||
|
disableScroll: false, |
||||
|
}) |
@ -0,0 +1,375 @@ |
|||||
|
<script setup> |
||||
|
import { ref } from 'vue'; |
||||
|
import Taro from '@tarojs/taro'; |
||||
|
import deviceIcon from '../../assets/deviceIcon.png' |
||||
|
import TabBar from '../../components/TabBar.vue' |
||||
|
// import { storeToRefs } from 'pinia'; |
||||
|
// import { useSupervisionStore } from '../../stores'; |
||||
|
|
||||
|
Taro.hideHomeButton(); |
||||
|
|
||||
|
// 虚拟数据 |
||||
|
const fieldList = ref([ |
||||
|
{ |
||||
|
recordId: 270424022978560, |
||||
|
studentId: 7, |
||||
|
studentName: '阿亮', |
||||
|
teacherId: 27, |
||||
|
teacherName: '侯老师', |
||||
|
droneId: 42, |
||||
|
droneSn: 'DJI-001', |
||||
|
airfield: { |
||||
|
id: 3, |
||||
|
name: '示范飞行场地2', |
||||
|
imageUrl: deviceIcon |
||||
|
}, |
||||
|
details: [ |
||||
|
{ |
||||
|
recordId: 270424022978560, |
||||
|
happen_time: '2025-03-04 11:16:47', |
||||
|
phase: '悬停', |
||||
|
audioCode: 'welcome', |
||||
|
isError: true, |
||||
|
errorDesc: '未按指定高度悬停' |
||||
|
} |
||||
|
], |
||||
|
failTimes: 1, |
||||
|
isPass: false, |
||||
|
startTime: '2025-03-05 17:19:25', |
||||
|
endTime: '2025-03-05 17:19:35', |
||||
|
inProgress: false |
||||
|
}, |
||||
|
{ |
||||
|
recordId: 270424022978561, |
||||
|
studentId: 8, |
||||
|
studentName: '小明', |
||||
|
teacherId: 28, |
||||
|
teacherName: '张老师', |
||||
|
droneId: 43, |
||||
|
droneSn: 'DJI-002', |
||||
|
airfield: { |
||||
|
id: 4, |
||||
|
name: '高级训练场', |
||||
|
imageUrl: deviceIcon |
||||
|
}, |
||||
|
details: [], |
||||
|
failTimes: 0, |
||||
|
isPass: true, |
||||
|
startTime: '2025-03-05 18:00:00', |
||||
|
endTime: '2025-03-05 18:15:00', |
||||
|
inProgress: false |
||||
|
}, |
||||
|
{ |
||||
|
recordId: 270424022978562, |
||||
|
studentId: 9, |
||||
|
studentName: '小红', |
||||
|
teacherId: 29, |
||||
|
teacherName: '王老师', |
||||
|
droneId: 44, |
||||
|
droneSn: 'DJI-003', |
||||
|
airfield: { |
||||
|
id: 5, |
||||
|
name: '初级训练场', |
||||
|
imageUrl: deviceIcon |
||||
|
}, |
||||
|
details: [ |
||||
|
{ |
||||
|
recordId: 270424022978562, |
||||
|
happen_time: '2025-03-05 19:10:30', |
||||
|
phase: '起飞', |
||||
|
audioCode: 'takeoff', |
||||
|
isError: true, |
||||
|
errorDesc: '起飞速度过快' |
||||
|
}, |
||||
|
{ |
||||
|
recordId: 270424022978562, |
||||
|
happen_time: '2025-03-05 19:12:00', |
||||
|
phase: '降落', |
||||
|
audioCode: 'landing', |
||||
|
isError: true, |
||||
|
errorDesc: '降落点偏离指定位置' |
||||
|
} |
||||
|
], |
||||
|
failTimes: 2, |
||||
|
isPass: false, |
||||
|
startTime: '2025-03-05 19:10:00', |
||||
|
endTime: '2025-03-05 19:15:00', |
||||
|
inProgress: true |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
// 暂时注释掉原有的API调用代码 |
||||
|
// const { getFieldList } = useSupervisionStore(); |
||||
|
// const { fieldList } = storeToRefs(useSupervisionStore()); |
||||
|
|
||||
|
Taro.useDidShow(() => { |
||||
|
// 使用虚拟数据,暂时注释掉API调用 |
||||
|
// getFieldList().catch(err => { |
||||
|
// Taro.showToast({ |
||||
|
// title: err?.description || '获取场地列表失败', |
||||
|
// icon: 'none' |
||||
|
// }); |
||||
|
// }); |
||||
|
}); |
||||
|
|
||||
|
function onRefresh() { |
||||
|
// 使用虚拟数据,暂时注释掉API调用 |
||||
|
// return getFieldList().finally(() => { |
||||
|
// Taro.stopPullDownRefresh(); |
||||
|
// }); |
||||
|
Taro.stopPullDownRefresh(); |
||||
|
} |
||||
|
|
||||
|
Taro.usePullDownRefresh(() => { |
||||
|
onRefresh(); |
||||
|
}); |
||||
|
|
||||
|
function onNavTo(id) { |
||||
|
Taro.navigateTo({ |
||||
|
url: `/pages/supervisionMap/index?id=${id}`, |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div class="field-list"> |
||||
|
<div class="field-card" v-for="item in fieldList" :key="item.recordId" @click="onNavTo(item.id)"> |
||||
|
<div class="field-image"> |
||||
|
<img :src="item.airfield.imageUrl" alt="场地图片" /> |
||||
|
<div class="status-badge" :class="{ 'in-progress': item.inProgress, 'passed': item.isPass, 'failed': !item.isPass && !item.inProgress }"> |
||||
|
{{ item.inProgress ? '进行中' : (item.isPass ? '通过' : '未通过') }} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="field-info"> |
||||
|
<div class="student-info"> |
||||
|
<div class="student-name">{{ item.studentName }}</div> |
||||
|
<div class="teacher-name">指导教师:{{ item.teacherName }}</div> |
||||
|
</div> |
||||
|
<div class="field-name"> |
||||
|
<span class="icon">📍</span> |
||||
|
{{ item.airfield.name }} |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<span class="label">飞机编号:</span> |
||||
|
<span class="value">{{ item.droneSn || '暂无' }}</span> |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<span class="label">开始时间:</span> |
||||
|
<span class="value">{{ item.startTime }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<TabBar /> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.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; |
||||
|
|
||||
|
img { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
.status-badge { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
right: 12px; |
||||
|
padding: 4px 12px; |
||||
|
border-radius: 12px; |
||||
|
font-size: 12px; |
||||
|
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; |
||||
|
|
||||
|
.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 { |
||||
|
font-size: 14px; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
.label { |
||||
|
color: #666; |
||||
|
margin-right: 8px; |
||||
|
flex-shrink: 0; |
||||
|
min-width: 70px; |
||||
|
} |
||||
|
|
||||
|
.value { |
||||
|
color: #333; |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.basic-info { |
||||
|
background-color: #f8f8f8; |
||||
|
border-radius: 8px; |
||||
|
padding: 12px; |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
.info-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 6px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.label { |
||||
|
color: #666; |
||||
|
font-size: 13px; |
||||
|
margin-right: 8px; |
||||
|
flex-shrink: 0; |
||||
|
min-width: 70px; |
||||
|
} |
||||
|
|
||||
|
.value { |
||||
|
color: #1a1a1a; |
||||
|
font-size: 13px; |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.error-info { |
||||
|
background-color: #fff2f0; |
||||
|
border-radius: 8px; |
||||
|
padding: 12px; |
||||
|
|
||||
|
.error-title { |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
color: #ff4d4f; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.error-list { |
||||
|
.error-item { |
||||
|
padding: 8px; |
||||
|
background-color: rgba(255, 77, 79, 0.1); |
||||
|
border-radius: 4px; |
||||
|
margin-bottom: 8px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.error-phase { |
||||
|
font-size: 13px; |
||||
|
font-weight: 500; |
||||
|
color: #ff4d4f; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.error-desc { |
||||
|
font-size: 12px; |
||||
|
color: #666; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.error-time { |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,5 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '监管详情', |
||||
|
navigationStyle: 'default', |
||||
|
pageOrientation: 'landscape' |
||||
|
}) |
@ -0,0 +1,289 @@ |
|||||
|
<script setup> |
||||
|
import { onMounted, ref, onUnmounted } from "vue"; |
||||
|
import * as Taro from "@tarojs/taro"; |
||||
|
import * as turf from '@turf/turf'; |
||||
|
import deviceCruise from '../../core/deviceCruise'; |
||||
|
// import TrackPlayback from '../../components/TrackPlayback.vue'; |
||||
|
import SupervisionData from '../../components/SupervisionData.vue'; |
||||
|
import SupervisionSideData from '../../components/SupervisionSideData.vue'; |
||||
|
import trackData from '../flightMap/track.json'; |
||||
|
import { convertToTrajectory, creatEightShaped } from '../flightMap/utils'; |
||||
|
|
||||
|
// 地图中心点位置 |
||||
|
const centerPoint = ref({ |
||||
|
lat: 39.908692, |
||||
|
lng: 116.397477 |
||||
|
}); |
||||
|
|
||||
|
// 地图上下文 |
||||
|
let mapContext = null; |
||||
|
|
||||
|
const show = ref(true); |
||||
|
|
||||
|
// 初始化地图 |
||||
|
onMounted(() => { |
||||
|
mapContext = Taro.createMapContext('map'); |
||||
|
show.value = true; |
||||
|
initMap(); |
||||
|
initEightShaped(); |
||||
|
}); |
||||
|
|
||||
|
// 多边形配置 |
||||
|
const polygons = ref([]); |
||||
|
// 圆形标记配置 |
||||
|
const circles = ref([]); |
||||
|
// 标记点配置 |
||||
|
const markerPoints = ref([]); |
||||
|
|
||||
|
// 轨迹数据转换 |
||||
|
const trajectory = convertToTrajectory(trackData, { |
||||
|
startTime: "2025-03-14T08:00:00Z", |
||||
|
interval: 1000 // 1秒间隔 |
||||
|
}); |
||||
|
|
||||
|
// 地图旋转角度 |
||||
|
const bearing = ref(0); |
||||
|
|
||||
|
// 飞行轨迹配置 |
||||
|
const polyline = ref([{ |
||||
|
points: trackData.map(coord => ({ |
||||
|
latitude: coord[1], |
||||
|
longitude: coord[0] |
||||
|
})), |
||||
|
color: '#008000', |
||||
|
width: 4, |
||||
|
arrowLine: true |
||||
|
}]); |
||||
|
|
||||
|
// 错误点位标记 |
||||
|
// const errorPoints = ref([{ |
||||
|
// latitude: 39.908692, |
||||
|
// longitude: 116.397477, |
||||
|
// iconPath: '../../assets/red-marker.svg', |
||||
|
// width: 30, |
||||
|
// height: 30, |
||||
|
// callout: { |
||||
|
// content: '错误点位', |
||||
|
// color: '#FF0000', |
||||
|
// fontSize: 14, |
||||
|
// borderRadius: 4, |
||||
|
// bgColor: '#FFFFFF', |
||||
|
// padding: 8, |
||||
|
// display: 'ALWAYS' |
||||
|
// } |
||||
|
// }]); |
||||
|
|
||||
|
// 初始化地图 |
||||
|
function initMap() { |
||||
|
if (!mapContext) return; |
||||
|
// |
||||
|
} |
||||
|
|
||||
|
// 地图旋转角度 |
||||
|
// const bearing = ref(0); |
||||
|
|
||||
|
// 初始化八字飞行 |
||||
|
function initEightShaped() { |
||||
|
if (!mapContext) return; |
||||
|
|
||||
|
const center1 = [116.397477, 39.908692]; |
||||
|
const center2 = [116.399477, 39.910692]; |
||||
|
|
||||
|
const mid = turf.midpoint(center1, center2).geometry.coordinates; |
||||
|
console.log('mid', mid); |
||||
|
|
||||
|
centerPoint.value = { |
||||
|
lat: mid[1], |
||||
|
lng: mid[0] |
||||
|
}; |
||||
|
// 计算两个圆心之间的方位角并转换为地图旋转角度 |
||||
|
let angle = turf.bearing(center1, center2); |
||||
|
|
||||
|
// 计算与正东方向的夹角 |
||||
|
angle = ((90 - angle) % 360 + 360) % 360; |
||||
|
|
||||
|
const { polygons: shapePolygons, circles: shapeCircles, markers: shapeMarkers } = creatEightShaped({ |
||||
|
center: center1, |
||||
|
radius: 105, |
||||
|
radiusDiff: 35, |
||||
|
centerWidth: 2, |
||||
|
}, { |
||||
|
center: center2, |
||||
|
radius: 105, |
||||
|
radiusDiff: 35, |
||||
|
centerWidth: 2, |
||||
|
}, [6, 0, 1, 3, 4, 5, 2], { radius: 50 }); |
||||
|
|
||||
|
// 更新地图显示数据 |
||||
|
polygons.value = shapePolygons; |
||||
|
markerPoints.value = shapeMarkers; |
||||
|
circles.value = shapeCircles; |
||||
|
|
||||
|
// 设置地图视野 |
||||
|
mapContext.includePoints({ |
||||
|
points: [ |
||||
|
{ latitude: center1[1], longitude: center1[0] }, |
||||
|
{ latitude: center2[1], longitude: center2[0] } |
||||
|
], |
||||
|
padding: [50, 50, 110, 50], |
||||
|
success: (res) => { |
||||
|
setTimeout(() => { |
||||
|
// 调整偏移量使八字飞行路径保持垂直显示 |
||||
|
bearing.value = angle; |
||||
|
// console.log('angle', bearing.value); |
||||
|
}, 5000); |
||||
|
// bearing.value = angle.toFixed(2) - 85; |
||||
|
console.log('地图视野设置成功', res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
console.error('地图视野设置失败', err); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 组件卸载时清理资源 |
||||
|
onUnmounted(() => { |
||||
|
if (deviceCruise) { |
||||
|
deviceCruise.destroy(); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<view :class="s.root"> |
||||
|
<view class="mapBox"> |
||||
|
<map |
||||
|
id="map" |
||||
|
:longitude="centerPoint.lng" |
||||
|
:latitude="centerPoint.lat" |
||||
|
:markers="markerPoints" |
||||
|
:polygons="polygons" |
||||
|
:circles="circles" |
||||
|
:polyline="polyline" |
||||
|
:enable-rotate="true" |
||||
|
:rotate="bearing" |
||||
|
:enable-satellite="true" |
||||
|
/> |
||||
|
<view v-if="show" class="exam-result-modal"> |
||||
|
<view class="modal-content"> |
||||
|
<view class="close-btn" @tap="show = false">×</view> |
||||
|
<view class="result-status" :class="{ 'pass': true }"> |
||||
|
通过 |
||||
|
</view> |
||||
|
<view class="info-item"> |
||||
|
<text class="label">姓名:</text> |
||||
|
<text class="value">张三</text> |
||||
|
</view> |
||||
|
<view class="info-item"> |
||||
|
<text class="label">飞机编号:</text> |
||||
|
<text class="value">UAV-001</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
<SupervisionData class="real-time-data" /> |
||||
|
<SupervisionSideData /> |
||||
|
</view> |
||||
|
</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%; |
||||
|
} |
||||
|
|
||||
|
.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,14 @@ |
|||||
|
/** |
||||
|
* stores |
||||
|
*/ |
||||
|
import { createPinia } from 'pinia'; |
||||
|
|
||||
|
const store = createPinia(); |
||||
|
|
||||
|
export default store; |
||||
|
export * from './modules/authStore'; |
||||
|
export * from './modules/flightStore'; |
||||
|
export * from './modules/routePlanStore'; |
||||
|
export * from './modules/returnTripStore'; |
||||
|
export * from './modules/evaluationStore'; |
||||
|
|
@ -0,0 +1,138 @@ |
|||||
|
/** |
||||
|
* authStore |
||||
|
*/ |
||||
|
import { ref, computed } from 'vue'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
import http from '../../utils/http'; |
||||
|
import * as helpers from '../../utils/helpers'; |
||||
|
import * as UserInfo from '../../utils/userInfo'; |
||||
|
import * as auth from '../../utils/auth'; |
||||
|
import * as urls from '../../config/urls'; |
||||
|
|
||||
|
export const useAuthStore = defineStore('auth', () => { |
||||
|
// state
|
||||
|
const userInfo = ref(UserInfo.get()); |
||||
|
|
||||
|
const checkRole = (roleCode) => computed(() => { |
||||
|
if (!userInfo.value?.roles) return false; |
||||
|
return userInfo.value.roles.find(({ code }) => code === roleCode); |
||||
|
}); |
||||
|
|
||||
|
const isSuperAdmin = checkRole('super_admin'); |
||||
|
const isOrgAdmin = checkRole('org_admin'); |
||||
|
const isTeacherAdmin = checkRole('teacher_admin'); |
||||
|
const isEduAdmin = checkRole('edu_admin'); |
||||
|
const isTeacher = checkRole('teacher'); |
||||
|
const isStudent = checkRole('student'); |
||||
|
|
||||
|
// actions
|
||||
|
function loginWithPassword(formData = {}) { |
||||
|
const requestDate = helpers.pick(formData, [ |
||||
|
'password', |
||||
|
'phone', |
||||
|
]); |
||||
|
|
||||
|
return http.post(urls.LOGIN_WITH_PASSWORD, requestDate, { |
||||
|
withToken: false |
||||
|
}).then(({ data: { data } = {} }) => { |
||||
|
// const { data = {} } = data;
|
||||
|
// 获取信息
|
||||
|
console.log('loginWithPassword', data); |
||||
|
|
||||
|
const { token: accessToken, user } = data; |
||||
|
if (accessToken) { |
||||
|
auth.saveToken(accessToken); |
||||
|
} |
||||
|
// 更新信息
|
||||
|
const newUserInfo = { |
||||
|
// ...userInfo.value,
|
||||
|
...(user || {}), |
||||
|
}; |
||||
|
userInfo.value = { ...newUserInfo }; |
||||
|
// // 保存信息
|
||||
|
UserInfo.save(newUserInfo); |
||||
|
|
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function wechatAuth({ code, sceneId } = {}) { |
||||
|
return http.post(urls.WECHAT_AUTH_URL, { |
||||
|
code, |
||||
|
sceneId |
||||
|
}, { |
||||
|
withToken: false |
||||
|
}).then(({ data: { data } = {} }) => { |
||||
|
console.log('wechatAuth', data); |
||||
|
const { token: accessToken = '', student = {}, user = {} } = data || {}; |
||||
|
if (accessToken) { |
||||
|
auth.saveToken(accessToken); |
||||
|
// 更新信息
|
||||
|
const newUserInfo = { |
||||
|
// ...userInfo.value,
|
||||
|
...(user || {}), |
||||
|
...(student || {}), |
||||
|
}; |
||||
|
userInfo.value = { ...newUserInfo }; |
||||
|
// // 保存信息
|
||||
|
UserInfo.save(newUserInfo); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getWechatUserInfo({ code, iv, encryptedData } = {}) { |
||||
|
return http.post(urls.GET_WECHAT_USERINFO, { |
||||
|
code, |
||||
|
iv, |
||||
|
encryptedData, |
||||
|
}).then(({ data: { data } = {} }) => { |
||||
|
console.log('wechatuserInfo', data); |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function updateStudentAvatar(formData = {}) { |
||||
|
const requestData = helpers.pick(formData, [ |
||||
|
'id', |
||||
|
'avatar', |
||||
|
]); |
||||
|
return http.put(urls.UPDATE_STUDENT(requestData.id), requestData).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function updateMemberAvatar(formData = {}) { |
||||
|
const requestData = helpers.pick(formData, [ |
||||
|
'id', |
||||
|
'avatar', |
||||
|
]); |
||||
|
return http.put(urls.UPDATE_MEMBER(requestData.id), requestData).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function uploadFile(formData, { header = {} } = {}) { |
||||
|
return http.post(urls.UPLOAD, formData, { header }).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
userInfo, |
||||
|
loginWithPassword, |
||||
|
isSuperAdmin, |
||||
|
isOrgAdmin, |
||||
|
isTeacherAdmin, |
||||
|
isEduAdmin, |
||||
|
isTeacher, |
||||
|
isStudent, |
||||
|
wechatAuth, |
||||
|
updateStudentAvatar, |
||||
|
updateMemberAvatar, |
||||
|
getWechatUserInfo, |
||||
|
uploadFile |
||||
|
}; |
||||
|
}); |
||||
|
export default {}; |
@ -0,0 +1,61 @@ |
|||||
|
/** |
||||
|
* evaluationStore |
||||
|
*/ |
||||
|
import { ref } from 'vue'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
import http from '../../utils/http'; |
||||
|
import * as urls from '../../config/urls'; |
||||
|
|
||||
|
export const useEvaluationStore = defineStore('evaluation', () => { |
||||
|
// state
|
||||
|
const evaluationList = ref([]); |
||||
|
const evaluationExtra = ref({ total: null }); |
||||
|
const evaluationQueries = ref({ pageNum: 1, pageSize: 10 }); |
||||
|
|
||||
|
// actions
|
||||
|
function getEvaluationList(params = {}) { |
||||
|
const queryParams = { ...evaluationQueries.value, ...params }; |
||||
|
return http.get(urls.GET_EVALUATION_LIST, { params: queryParams }).then(({ data: { data } = {} }) => { |
||||
|
const { records: list = [], total = 0 } = data; |
||||
|
evaluationList.value = list || []; |
||||
|
evaluationExtra.value = { ...evaluationExtra.value, total }; |
||||
|
evaluationQueries.value = { ...evaluationQueries.value, ...queryParams }; |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function createEvaluation(params) { |
||||
|
return http.post(urls.CREATE_EVALUATION, params).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function updateEvaluation(params) { |
||||
|
return http.put(urls.UPDATE_EVALUATION, params).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getEvaluationDetail(id) { |
||||
|
return http.get(urls.GET_EVALUATION_DETAIL(id)).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function replyEvaluation(evaluationId, content) { |
||||
|
return http.post(urls.REPLY_EVALUATION, { evaluationId, content }).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
evaluationList, |
||||
|
evaluationExtra, |
||||
|
evaluationQueries, |
||||
|
getEvaluationList, |
||||
|
createEvaluation, |
||||
|
updateEvaluation, |
||||
|
getEvaluationDetail, |
||||
|
replyEvaluation |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,49 @@ |
|||||
|
/** |
||||
|
* flightStore |
||||
|
*/ |
||||
|
import { ref } from 'vue'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
import http from '../../utils/http'; |
||||
|
import * as urls from '../../config/urls'; |
||||
|
|
||||
|
export const useFlightStore = defineStore('flight', () => { |
||||
|
// state
|
||||
|
const flightList = ref([]); |
||||
|
const flightExtra = ref({ total: null }); |
||||
|
const flightQueries = ref({ pageNum: 1, pageSize: 10 }); |
||||
|
|
||||
|
// actions
|
||||
|
function getFlightList(params = {}) { |
||||
|
const queryParams = { ...flightQueries.value, ...params }; |
||||
|
return http.get(urls.GET_FLIGHT_LIST, { params: queryParams }).then(({ data: { data } = {} }) => { |
||||
|
const { records: list = [], total = 0 } = data; |
||||
|
flightList.value = list || []; |
||||
|
flightExtra.value = { ...flightExtra.value, total }; |
||||
|
flightQueries.value = { ...flightQueries.value, ...queryParams }; |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getFlightDetail(id) { |
||||
|
return http.get(urls.GET_FLIGHT_DETAIL(id)).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getFlightTracks(id) { |
||||
|
return http.get(urls.GET_FLIGHT_TRACKS(id)).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
return { |
||||
|
flightList, |
||||
|
flightExtra, |
||||
|
flightQueries, |
||||
|
getFlightList, |
||||
|
getFlightDetail, |
||||
|
getFlightTracks |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,40 @@ |
|||||
|
/** |
||||
|
* returnTripStore |
||||
|
*/ |
||||
|
import { ref } from 'vue'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
import http from '../../utils/http'; |
||||
|
import * as urls from '../../config/urls'; |
||||
|
|
||||
|
export const useReturnTripStore = defineStore('returnTrip', () => { |
||||
|
// state
|
||||
|
const returnTripList = ref([]); |
||||
|
const returnTripExtra = ref({ total: null }); |
||||
|
const returnTripQueries = ref({ pageNum: 1, pageSize: 10 }); |
||||
|
|
||||
|
// actions
|
||||
|
function getReturnTripList(params = {}) { |
||||
|
const queryParams = { ...returnTripQueries.value, ...params }; |
||||
|
return http.get(urls.GET_RETURN_TRIP_LIST, { params: queryParams }).then(({ data: { data } = {} }) => { |
||||
|
const { records: list = [], total = 0 } = data; |
||||
|
returnTripList.value = list || []; |
||||
|
returnTripExtra.value = { ...returnTripExtra.value, total }; |
||||
|
returnTripQueries.value = { ...returnTripQueries.value, ...queryParams }; |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getReturnTripDetail(id) { |
||||
|
return http.get(urls.GET_RETURN_TRIP_DETAIL(id)).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
returnTripList, |
||||
|
returnTripExtra, |
||||
|
returnTripQueries, |
||||
|
getReturnTripList, |
||||
|
getReturnTripDetail |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,40 @@ |
|||||
|
/** |
||||
|
* routePlanStore |
||||
|
*/ |
||||
|
import { ref } from 'vue'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
import http from '../../utils/http'; |
||||
|
import * as urls from '../../config/urls'; |
||||
|
|
||||
|
export const useRoutePlanStore = defineStore('routePlan', () => { |
||||
|
// state
|
||||
|
const routePlanList = ref([]); |
||||
|
const routePlanExtra = ref({ total: null }); |
||||
|
const routePlanQueries = ref({ pageNum: 1, pageSize: 10 }); |
||||
|
|
||||
|
// actions
|
||||
|
function getRoutePlanList(params = {}) { |
||||
|
const queryParams = { ...routePlanQueries.value, ...params }; |
||||
|
return http.get(urls.GET_ROUTE_PLAN_LIST, { params: queryParams }).then(({ data: { data } = {} }) => { |
||||
|
const { records: list = [], total = 0 } = data; |
||||
|
routePlanList.value = list || []; |
||||
|
routePlanExtra.value = { ...routePlanExtra.value, total }; |
||||
|
routePlanQueries.value = { ...routePlanQueries.value, ...queryParams }; |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getRoutePlanDetail(id) { |
||||
|
return http.get(urls.GET_ROUTE_PLAN_DETAIL(id)).then(({ data }) => { |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
routePlanList, |
||||
|
routePlanExtra, |
||||
|
routePlanQueries, |
||||
|
getRoutePlanList, |
||||
|
getRoutePlanDetail |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,51 @@ |
|||||
|
/** |
||||
|
* supervisionStore |
||||
|
* 场地监管相关接口 |
||||
|
*/ |
||||
|
import { ref } from 'vue'; |
||||
|
import { defineStore } from 'pinia'; |
||||
|
import * as urls from '../../config/urls'; |
||||
|
import http from '../../utils/http'; |
||||
|
|
||||
|
export const useSupervisionStore = defineStore('supervision', () => { |
||||
|
const fieldList = ref([]); |
||||
|
|
||||
|
const flightQueries = ref({ pageNum: 1, pageSize: 99999, inProgress: true }); |
||||
|
|
||||
|
// actions
|
||||
|
function getFieldList() { |
||||
|
return http.get(urls.GET_FLIGHT_LIST, { params: flightQueries }).then(({ data: { data } = {} }) => { |
||||
|
const { records: list = [], total = 0 } = data; |
||||
|
|
||||
|
// 获取所有记录的详情信息
|
||||
|
const detailPromises = list.map(record => { |
||||
|
return http.get(urls.GET_FLIGHT_DETAIL(record.id)).then(({ data }) => { |
||||
|
return { ...record, ...data }; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// 使用Promise.all并行处理所有详情请求
|
||||
|
return Promise.all(detailPromises).then(detailedList => { |
||||
|
fieldList.value = detailedList; |
||||
|
return { ...data, records: detailedList }; |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// function getFieldDetail(id) {
|
||||
|
// return http.get(urls.GET_FLIGHT_DETAIL(id)).then(({ data }) => {
|
||||
|
// return data;
|
||||
|
// });
|
||||
|
// }
|
||||
|
|
||||
|
// function getFlightTracks(id) {
|
||||
|
// return http.get(urls.GET_FLIGHT_TRACKS(id)).then(({ data }) => {
|
||||
|
// return data;
|
||||
|
// });
|
||||
|
// }
|
||||
|
|
||||
|
return { |
||||
|
fieldList, |
||||
|
getFieldList, |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* token信息存取 |
||||
|
*/ |
||||
|
import { setStorageSync, getStorageSync, removeStorageSync } from "@tarojs/taro"; |
||||
|
|
||||
|
export function saveToken(accessToken) { |
||||
|
try { |
||||
|
setStorageSync('token', accessToken) |
||||
|
} catch (e) { |
||||
|
// Do something when catch error
|
||||
|
console.log('e', e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function getToken() { |
||||
|
let token; |
||||
|
try { |
||||
|
token = getStorageSync('token'); |
||||
|
} catch (e) { |
||||
|
// Do something when catch error
|
||||
|
console.log('e', e); |
||||
|
} |
||||
|
return token; |
||||
|
} |
||||
|
|
||||
|
export function removeToken() { |
||||
|
try { |
||||
|
removeStorageSync('token') |
||||
|
} catch (e) { |
||||
|
// Do something when catch error
|
||||
|
console.log('e', e); |
||||
|
} |
||||
|
} |
||||
|
|
@ -0,0 +1,6 @@ |
|||||
|
/** |
||||
|
* 事件总线(用于跨组件通信,一般用于非父子组件) |
||||
|
*/ |
||||
|
// import mitt from 'mitt';
|
||||
|
|
||||
|
// export default mitt();
|
@ -0,0 +1,210 @@ |
|||||
|
/** |
||||
|
* 辅助函数 |
||||
|
*/ |
||||
|
import dayjs from 'dayjs'; |
||||
|
|
||||
|
/** |
||||
|
* 等待异步结果 |
||||
|
* @param checker 检测函数,返回值为truthy时表示得到想要的结果,否则反之 |
||||
|
* @param timeout 指定时间内没有得到想要的结果则超时 |
||||
|
*/ |
||||
|
export function until(checker = () => true, timeout = 2000) { |
||||
|
return new Promise((resolve) => { |
||||
|
let pollingTimer = null; |
||||
|
|
||||
|
const timeoutTimer = setTimeout(() => { |
||||
|
clearInterval(pollingTimer); |
||||
|
resolve(false); |
||||
|
}, timeout); |
||||
|
|
||||
|
pollingTimer = setInterval(() => { |
||||
|
const result = checker(); |
||||
|
if (result) { |
||||
|
clearTimeout(timeoutTimer); |
||||
|
clearInterval(pollingTimer); |
||||
|
resolve(result); |
||||
|
} |
||||
|
}, 10); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据restful的uri构建实际url |
||||
|
* @param uri uri模版 |
||||
|
* @param argv 插入到uri中的参数列表,按顺序插入 |
||||
|
* @returns url |
||||
|
*/ |
||||
|
export function buildURL(uri, ...argv) { |
||||
|
return uri.replace(/{\w+}|:[a-zA-Z]+/g, () => { |
||||
|
const res = argv.shift(); |
||||
|
if (res === undefined) { |
||||
|
throw new Error('URI 参数不足'); |
||||
|
} |
||||
|
return res; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 选取原object指定属性,构成新的object |
||||
|
* @param originObject |
||||
|
* @param pickKeys |
||||
|
*/ |
||||
|
export function pick(originObject, pickKeys = []) { |
||||
|
const newObject = {}; |
||||
|
pickKeys.forEach((key) => { |
||||
|
if (Object.prototype.hasOwnProperty.call(originObject, key)) { |
||||
|
newObject[key] = originObject[key]; |
||||
|
} |
||||
|
}); |
||||
|
return newObject; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新target对象的键值,使用source对象里的数据 |
||||
|
* @param target |
||||
|
* @param source |
||||
|
*/ |
||||
|
export function update(target, source) { |
||||
|
const result = { ...target }; |
||||
|
Object.keys(target).forEach((key) => { |
||||
|
if (Object.prototype.hasOwnProperty.call(source, key)) { |
||||
|
result[key] = source[key]; |
||||
|
} |
||||
|
}); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将对象的部分key重命名 |
||||
|
* @param originObject 原始对象(也可以是相同结构的对象构成的数组) |
||||
|
* @param keysMapping 新keys映射(如{ name: 'username', sex: 'gender' },表示把原始对象中的name重命名为username、把sex重命名为gender,原始对象其他key不变) |
||||
|
* @returns {{}|*} 新对象 |
||||
|
*/ |
||||
|
export function renameKeys(originObject, keysMapping = {}) { |
||||
|
if (!Array.isArray(originObject)) { |
||||
|
const kvList = Object.keys(originObject).map((key) => { |
||||
|
const newKey = keysMapping[key] || key; |
||||
|
return { [newKey]: originObject[key] }; |
||||
|
}); |
||||
|
return Object.assign({}, ...kvList); |
||||
|
} |
||||
|
return originObject.map((obj) => renameKeys(obj, keysMapping)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 随机数 |
||||
|
*/ |
||||
|
export function rand(min, max, fraction = 0) { |
||||
|
const res = (Math.random() * ((max - min) + 1)) + min; |
||||
|
return res.toFixed(fraction) - 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将input当着数字处理,若不是数字则返回代替字符 |
||||
|
* @param input |
||||
|
* @param substitution |
||||
|
* @returns {string|number} |
||||
|
*/ |
||||
|
export function numeric(input, substitution = '-') { |
||||
|
const type = typeof input; |
||||
|
if (type !== 'number' && type !== 'string') return substitution; |
||||
|
|
||||
|
const output = Number(input); |
||||
|
return !Number.isNaN(output) ? output : substitution; |
||||
|
} |
||||
|
|
||||
|
// 保留小数位(是整数则不保留)
|
||||
|
export const toFixed = (num, digits = 2) => (Number(num || 0).toFixed(digits)) - 0; |
||||
|
|
||||
|
|
||||
|
export const falsyTo = (fv, to) => (fv || fv === 0 ? fv : to); |
||||
|
/** |
||||
|
* 格式化时间戳 |
||||
|
* @param timestamp |
||||
|
* @param format |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
export function formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') { |
||||
|
return (timestamp || timestamp === 0) && dayjs(timestamp).isValid() ? dayjs(timestamp).format(format) : '-'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将总秒数格式化为“x时y分z秒”形式 |
||||
|
* @param sec |
||||
|
* @param precision |
||||
|
* @param isChinese |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
export function popularTime(sec, precision = 0, isChinese = false) { |
||||
|
const { HOUR, MINUTE, SECOND } = isChinese ? { HOUR: '时', MINUTE: '分', SECOND: '秒' } : { |
||||
|
HOUR: 'h', |
||||
|
MINUTE: 'm', |
||||
|
SECOND: 's' |
||||
|
}; |
||||
|
let remainingSec = sec; |
||||
|
const hours = Math.floor(remainingSec / 3600); |
||||
|
|
||||
|
remainingSec -= hours * 3600; |
||||
|
const minutes = Math.floor(remainingSec / 60); |
||||
|
|
||||
|
remainingSec -= minutes * 60; |
||||
|
|
||||
|
return `${hours ? `${hours}${HOUR}` : ''}${minutes ? `${minutes}${MINUTE}` : ''}${remainingSec ? `${remainingSec.toFixed(precision)}${SECOND}` : ''}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将秒转化为小时 |
||||
|
* @param sec |
||||
|
* @returns {number} |
||||
|
*/ |
||||
|
export function toHour(sec) { |
||||
|
return (sec / 3600).toFixed(2) - 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 对数组中的每个元素进行偏移计算,返回计算后的数组 |
||||
|
* @param originArray 原数组(有数字构成) |
||||
|
* @param offsetArray 偏移量数组(与原数组元素一一对应) |
||||
|
* @returns {*[]} 计算后的新数组 |
||||
|
*/ |
||||
|
export function arrOffset(originArray = [], offsetArray = []) { |
||||
|
return originArray.map((item, index) => +item + (offsetArray[index] || 0)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 范围时间内,生成连续时间戳(按步长连续) |
||||
|
* @param startTs 起始时间戳 |
||||
|
* @param endTs 结束时间戳 |
||||
|
* @param step 步长(year | month | week | day | hour | minute | second | millisecond) |
||||
|
* @returns {*[]} 连续时间戳 |
||||
|
*/ |
||||
|
export function rangeTimestamps(startTs, endTs, step = 'day') { |
||||
|
const s = dayjs(startTs).startOf(step); |
||||
|
const eTs = dayjs(endTs).startOf(step).valueOf(); |
||||
|
const result = []; |
||||
|
for (; s.valueOf() <= eTs;) { |
||||
|
result.push(s.valueOf()); |
||||
|
s.add(1, step); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算出数组中最多的元素 |
||||
|
* @param array |
||||
|
* @returns {*} |
||||
|
*/ |
||||
|
export function mostCommonElement(array) { |
||||
|
return array.reduce((max, element) => { |
||||
|
const count = array.filter((e) => e === element).length; |
||||
|
return count > array.filter((e) => e === max).length ? element : max; |
||||
|
}, ''); |
||||
|
} |
||||
|
|
||||
|
// /**
|
||||
|
// * 将gps坐标,转换为国测局坐标
|
||||
|
// * @param point 经纬度构成的数组[经度, 纬度]
|
||||
|
// */
|
||||
|
// export function GPS2GCJ(point) {
|
||||
|
// return gcoord.transform(point, gcoord.WGS84, gcoord.GCJ02);
|
||||
|
// }
|
@ -0,0 +1,209 @@ |
|||||
|
/** |
||||
|
* 请求 |
||||
|
*/ |
||||
|
import Taro, { request, downloadFile, uploadFile } from "@tarojs/taro"; |
||||
|
import { getToken } from './auth'; |
||||
|
// import { log } from "console";
|
||||
|
// import * as UserInfo from "./userInfo";
|
||||
|
// import * as Auth from "./auth";
|
||||
|
|
||||
|
// function judgmentHttpCode(res, reject, resolve) {
|
||||
|
// if (res?.statusCode !== 200 || res?.data?.code !== 200) {
|
||||
|
// reject(res?.data);
|
||||
|
// if (res?.data?.code === 40011) {
|
||||
|
// UserInfo.removeUserInfo();
|
||||
|
// Auth.removeToken();
|
||||
|
// Taro.reLaunch({ url: '/pages/login/index?refresh=true' })
|
||||
|
// }
|
||||
|
// } else {
|
||||
|
// resolve(res?.data || { code: SUCCESS_CODE, description: "无数据返回", data: {} });
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
class Http { |
||||
|
get = (url, { params = {}, header = {}, withToken = true } = {}) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
request({ |
||||
|
url, |
||||
|
header: { |
||||
|
...(withToken ? { |
||||
|
Authorization: `Bearer ${getToken()}` |
||||
|
} : {}), |
||||
|
...header, |
||||
|
}, |
||||
|
data: params, |
||||
|
method: 'GET', |
||||
|
success: (res) => { |
||||
|
const { data, statusCode } = res || {}; |
||||
|
if (statusCode !== 200) { |
||||
|
reject(data); |
||||
|
return; |
||||
|
} |
||||
|
resolve({ data }); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
reject({ msg: err.errMsg || '请求失败' }); |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
post = (url, data = {}, { header = {}, withToken = true } = {}) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
request({ |
||||
|
url, |
||||
|
header: { |
||||
|
...(withToken ? { |
||||
|
Authorization: `Bearer ${getToken()}` |
||||
|
} : {}), |
||||
|
...header, |
||||
|
}, |
||||
|
data, |
||||
|
method: 'POST', |
||||
|
success: (res) => { |
||||
|
const { data, statusCode } = res || {}; |
||||
|
if (statusCode !== 200) { |
||||
|
reject(data); |
||||
|
return; |
||||
|
} |
||||
|
resolve({ data }); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
reject({ msg: err.errMsg || '请求失败' }); |
||||
|
} |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
put = (url, data = {}, { header = {}, withToken = true } = {}) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
request({ |
||||
|
url, |
||||
|
header: { |
||||
|
...(withToken ? { |
||||
|
Authorization: `Bearer ${getToken()}` |
||||
|
} : {}), |
||||
|
...header, |
||||
|
}, |
||||
|
data, |
||||
|
method: 'PUT', |
||||
|
success: (res) => { |
||||
|
const { data, statusCode } = res || {}; |
||||
|
if (statusCode !== 200) { |
||||
|
reject(data); |
||||
|
return; |
||||
|
} |
||||
|
resolve({ data }); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
reject({ msg: err.errMsg || '请求失败' }); |
||||
|
} |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
delete = (url, data = {}, { header = {}, withToken = true } = {}) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
request({ |
||||
|
url, |
||||
|
header: { |
||||
|
...(withToken ? { |
||||
|
Authorization: `Bearer ${getToken()}` |
||||
|
} : {}), |
||||
|
...header, |
||||
|
}, |
||||
|
data, |
||||
|
method: 'DELETE', |
||||
|
success: (res) => { |
||||
|
const { data, statusCode } = res || {}; |
||||
|
if (statusCode !== 200) { |
||||
|
reject(data); |
||||
|
return; |
||||
|
} |
||||
|
resolve({ data }); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
reject({ msg: err.errMsg || '请求失败' }); |
||||
|
} |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// downLoadFile = (url, data = {}, { header = {}, withToken = true } = {}) => {
|
||||
|
// return new Promise((resolve, reject) => {
|
||||
|
// downloadFile({
|
||||
|
// url,
|
||||
|
// header: {
|
||||
|
// ...(withToken ? {
|
||||
|
// Authorization: `Bearer ${getToken()}`
|
||||
|
// } : {}),
|
||||
|
// ...header,
|
||||
|
// },
|
||||
|
// // data,
|
||||
|
// // method: 'DELETE',
|
||||
|
// success: (res) => {
|
||||
|
// // console.log('downloadFile', res);
|
||||
|
// if (res?.data) {
|
||||
|
// reject(res?.data);
|
||||
|
// } else {
|
||||
|
// const str = res.header["Content-disposition"];
|
||||
|
// // console.log('str', str);
|
||||
|
// const regex = /filename\*=utf-8''([^;]+)/;
|
||||
|
// const match = `${str}`.match(regex);
|
||||
|
//
|
||||
|
// let fileName = '未命名文件';
|
||||
|
// if (match && match[1]) {
|
||||
|
// fileName = decodeURIComponent(match[1]);
|
||||
|
// }
|
||||
|
// resolve({ code: SUCCESS_CODE, data: { filePath: res.tempFilePath, fileName } });
|
||||
|
// }
|
||||
|
// // if (res?.statusCode !== 200 || res?.data?.code !== 200) {
|
||||
|
// // reject(res?.data);
|
||||
|
// // } else {
|
||||
|
// // resolve(res?.data || { code: SUCCESS_CODE, description: "无数据返回", data: {} });
|
||||
|
// // }
|
||||
|
// },
|
||||
|
// fail: (err) => {
|
||||
|
// reject({ code: err?.errno || 0, description: err?.errMsg || '请求发送失败', data: {} });
|
||||
|
// }
|
||||
|
// })
|
||||
|
// });
|
||||
|
// }
|
||||
|
//
|
||||
|
// upLoad = (url, data = {}, { header = {}, withToken = true } = {}) => {
|
||||
|
// return new Promise((resolve, reject) => {
|
||||
|
// uploadFile({
|
||||
|
// url,
|
||||
|
// header: {
|
||||
|
// ...(withToken ? {
|
||||
|
// Authorization: `Bearer ${getToken()}`
|
||||
|
// } : {}),
|
||||
|
// ...header,
|
||||
|
// },
|
||||
|
// filePath: data.filePath,
|
||||
|
// name: 'no',
|
||||
|
// formData: data,
|
||||
|
// // data,
|
||||
|
// // method: 'DELETE',
|
||||
|
// success: (res) => {
|
||||
|
// console.log('downloadFile', res);
|
||||
|
// if (res?.data) {
|
||||
|
// reject(res?.data);
|
||||
|
// } else {
|
||||
|
// resolve({ code: SUCCESS_CODE, data: res.tempFilePath })
|
||||
|
// }
|
||||
|
// // if (res?.statusCode !== 200 || res?.data?.code !== 200) {
|
||||
|
// // reject(res?.data);
|
||||
|
// // } else {
|
||||
|
// // resolve(res?.data || { code: SUCCESS_CODE, description: "无数据返回", data: {} });
|
||||
|
// // }
|
||||
|
// },
|
||||
|
// fail: (err) => {
|
||||
|
// reject({ code: err?.errno || 0, description: err?.errMsg || '请求发送失败', data: {} });
|
||||
|
// }
|
||||
|
// })
|
||||
|
// });
|
||||
|
// }
|
||||
|
} |
||||
|
|
||||
|
export default new Http(); |
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 身份信息存取 |
||||
|
*/ |
||||
|
import { setStorageSync, getStorageSync, removeStorageSync } from "@tarojs/taro"; |
||||
|
|
||||
|
export function save(userInfo) { |
||||
|
try { |
||||
|
// JSON.stringify(userInfo)
|
||||
|
setStorageSync('userInfo', userInfo) |
||||
|
} catch (e) { |
||||
|
// Do something when catch error
|
||||
|
console.log('e', e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function get() { |
||||
|
let userInfo; |
||||
|
try { |
||||
|
userInfo = getStorageSync('userInfo'); |
||||
|
} catch (e) { |
||||
|
// Do something when catch error
|
||||
|
console.log('e', e); |
||||
|
} |
||||
|
return userInfo; |
||||
|
} |
||||
|
|
||||
|
export function remove() { |
||||
|
try { |
||||
|
removeStorageSync('userInfo') |
||||
|
} catch (e) { |
||||
|
// Do something when catch error
|
||||
|
console.log('e', e); |
||||
|
} |
||||
|
} |