+
-
![场地图片]()
-
- {{ item.inProgress ? '进行中' : (item.isPass ? '通过' : '未通过') }}
+
![场地图片]()
+
+ {{ '进行中' }}
-
{{ item.studentName }}
+
{{ item.className }}: {{ item.studentName }}
指导教师:{{ item.teacherName }}
📍
- {{ item.airfield.name }}
+ {{ item.airfieldName }}
飞机编号:
@@ -161,6 +110,9 @@ function onNavTo(id) {
+
+
+
@@ -179,26 +131,26 @@ function onNavTo(id) {
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%;
@@ -229,22 +181,22 @@ function onNavTo(id) {
}
}
}
-
+
.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;
@@ -253,7 +205,7 @@ function onNavTo(id) {
border-radius: 4px;
}
}
-
+
.field-name {
font-size: 14px;
color: #333;
@@ -263,31 +215,31 @@ function onNavTo(id) {
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;
@@ -299,16 +251,16 @@ function onNavTo(id) {
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;
@@ -316,7 +268,7 @@ function onNavTo(id) {
flex-shrink: 0;
min-width: 70px;
}
-
+
.value {
color: #1a1a1a;
font-size: 13px;
@@ -372,4 +324,4 @@ function onNavTo(id) {
}
}
}
-
\ No newline at end of file
+
diff --git a/src/pages/supervisionMap/BottomSide.vue b/src/pages/supervisionMap/BottomSide.vue
new file mode 100644
index 0000000..f3f4440
--- /dev/null
+++ b/src/pages/supervisionMap/BottomSide.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
连接中...
+
{{ info?.droneOnLine ? '已连接飞机' : '飞机失联..' }}
+
+
+
+
{{ info?.voltage || '-' }} v
+
+
+
+
{{ info?.height || '-' }} m
+
+
+
+
{{ info?.yaw || '-' }}
+
+
+
+
{{ info?.satellite || '-' }} {{ info?.fixTypeLabel || '-'}}
+
+
+
+
+
diff --git a/src/pages/supervisionMap/LeftSide.vue b/src/pages/supervisionMap/LeftSide.vue
new file mode 100644
index 0000000..fd56597
--- /dev/null
+++ b/src/pages/supervisionMap/LeftSide.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
{{ toFixed(info.timeTicker || 0, 1) }}
+
+
+
+
+
切线速度:
+
+
+
+
{{ toFixed(info?.speed || 0, 2) }}
+
+
+
航向偏差:
+
+
+
+
{{ toFixed(info?.angle || 0, 2) }}
+
+
+
高度偏差:
+
+
+
+
{{ toFixed(info?.vertical || 0, 2) }}
+
+
+
水平偏差:
+
+
+
+
{{ toFixed(info?.horizontal || 0, 2) }}
+
+
+
角速度:
+
+
+
+
{{ toFixed(info?.angleSpeed || 0, 1) }}
+
+
+
+
+
+
+
+
{{ toFixed(info?.yaw || 0, 1) }}
+
+
+
+ 返回首页
+
+
+
+
+
diff --git a/src/pages/supervisionMap/ResultModal.vue b/src/pages/supervisionMap/ResultModal.vue
new file mode 100644
index 0000000..d8f53b9
--- /dev/null
+++ b/src/pages/supervisionMap/ResultModal.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+ ×
+
+ {{ isPassed ? '通过' : '未通过' }}
+
+
+ 姓名:
+ {{ name }}
+
+
+ 飞机编号:
+ {{ uavId }}
+
+
+
+
+
+
diff --git a/src/pages/supervisionMap/RightSide.vue b/src/pages/supervisionMap/RightSide.vue
new file mode 100644
index 0000000..fb49b43
--- /dev/null
+++ b/src/pages/supervisionMap/RightSide.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
{{ flightDetail?.studentName || '-' }}
+
({{ flightDetail?.studentSn || '-' }})
+
多旋翼- {{ weightClasses[flightDetail?.weightClass] || '?' }} 无人机
+
+
+
+
+
+ {{ info?.text }}
+
+
+
+ {{ currentTime }}
+
+
+
+
+
diff --git a/src/pages/supervisionMap/geo.js b/src/pages/supervisionMap/geo.js
new file mode 100644
index 0000000..bf2b67c
--- /dev/null
+++ b/src/pages/supervisionMap/geo.js
@@ -0,0 +1,118 @@
+/**
+ * 地理空间函数库
+ */
+import * as math from 'mathjs';
+import * as turf from '@turf/turf';
+
+/**
+ * 将两个点连成一个向量(并确保落在1、4象限)
+ * @param point1
+ * @param point2
+ * @returns {Vector}
+ */
+export function pointToVector(point1, point2) {
+ const vec = math.subtract(point2, point1);
+ return vec[0] < 0 ? math.multiply(-1, vec) : vec;
+}
+
+/**
+ * 弧度2角度
+ * @param radians
+ * @returns {number}
+ */
+export function radToDeg(radians) {
+ return math.multiply(radians, math.divide(180, math.pi));
+}
+
+/**
+ * 计算二维向量与x轴的夹角(弧度)
+ * @param {number[]} vector - 二维向量 [x, y]
+ * @returns {number} 夹角的弧度值
+ */
+export function angleWithXAxis(vector) {
+ const [x] = vector;
+ const magnitude = math.norm(vector);
+
+ if (magnitude === 0) {
+ throw new Error('零向量的夹角未定义');
+ }
+
+ const cosTheta = x / magnitude;
+ // 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
+ const safeCosTheta = Math.max(-1, Math.min(1, cosTheta));
+
+ return math.acos(safeCosTheta);
+}
+
+/**
+ * 计算二维向量与 y 轴的夹角(弧度)
+ * @param {number[]} vector - 二维向量 [x, y]
+ * @returns {number} 夹角的弧度值
+ */
+export function angleWithYAxis(vector) {
+ const [, y] = vector;
+ const magnitude = math.norm(vector); // 计算向量的模长 ||v||
+
+ if (magnitude === 0) {
+ throw new Error('零向量的夹角未定义');
+ }
+
+ const cosTheta = y / magnitude;
+ // 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
+ const safeCosTheta = math.max(-1, math.min(1, cosTheta));
+
+ // 计算夹角 theta
+ return math.acos(safeCosTheta);
+}
+
+/**
+ * 计算两个二维向量之间的夹角(弧度)
+ * @param {number[]} vectorA - 第一个二维向量 [x1, y1]
+ * @param {number[]} vectorB - 第二个二维向量 [x2, y2]
+ * @returns {number} 夹角的弧度值
+ */
+export function angleBetweenVectors(vectorA, vectorB) {
+ const dotProduct = math.dot(vectorA, vectorB);
+ const magnitudeA = math.norm(vectorA);
+ const magnitudeB = math.norm(vectorB);
+
+ if (magnitudeA === 0 || magnitudeB === 0) {
+ throw new Error('其中一个向量是零向量,夹角未定义');
+ }
+
+ const cosTheta = dotProduct / (magnitudeA * magnitudeB);
+ // 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
+ const safeCosTheta = Math.max(-1, Math.min(1, cosTheta));
+
+ return math.acos(safeCosTheta);
+}
+
+/**
+ * 计算两个坐标点的中点
+ * @param point1
+ * @param point2
+ * @returns {number[]}
+ */
+export function midPoint(point1, point2) {
+ const [x1, y1] = point1;
+ const [x2, y2] = point2;
+ const x = (x1 + x2) / 2;
+ const y = (y1 + y2) / 2;
+ return [+x.toFixed(8), +y.toFixed(8)];
+}
+
+/**
+ * 生成圆形路径
+ * @param center 圆心([lng, lat])
+ * @param radius 半径(单位:米)
+ * @param steps 分段数
+ * @returns {any}
+ */
+export function genCirclePath(center, radius, steps = 64) {
+ const geojson = turf.circle(center, radius / 1000, {
+ steps,
+ units: 'kilometers',
+ });
+ const [coords] = turf.getCoords(geojson);
+ return coords;
+}
diff --git a/src/pages/supervisionMap/index.config.js b/src/pages/supervisionMap/index.config.js
index d6f70a8..da39e57 100644
--- a/src/pages/supervisionMap/index.config.js
+++ b/src/pages/supervisionMap/index.config.js
@@ -1,5 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '监管详情',
- navigationStyle: 'default',
+ navigationStyle: 'custom',
pageOrientation: 'landscape'
-})
\ No newline at end of file
+})
diff --git a/src/pages/supervisionMap/index.vue b/src/pages/supervisionMap/index.vue
index 2748e1c..84d2b4c 100644
--- a/src/pages/supervisionMap/index.vue
+++ b/src/pages/supervisionMap/index.vue
@@ -1,18 +1,34 @@
@@ -154,36 +290,25 @@
-
-
- ×
-
- 通过
-
-
- 姓名:
- 张三
-
-
- 飞机编号:
- UAV-001
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
+
diff --git a/src/pages/supervisionMap/useConnector.js b/src/pages/supervisionMap/useConnector.js
new file mode 100644
index 0000000..4391860
--- /dev/null
+++ b/src/pages/supervisionMap/useConnector.js
@@ -0,0 +1,224 @@
+import { createGlobalState, useWebSocket } from '@vueuse/core';
+import { computed, ref, watch } from 'vue';
+import * as turf from '@turf/turf';
+import gcoord from 'gcoord';
+import * as urls from '../config/urls';
+import * as geo from '../utils/geo';
+import { GPS_FIX_TYPE, GPS_FIX_TYPE2 } from '../config/gpsFixTypeMap';
+import { FLY_MODE } from '../config/flyModeMap';
+import { FC_SYSTEM_STATUS } from '../config/fcSystemStatus';
+import { ERRORS_COUNT_1, ERRORS_COUNT_2, ERRORS_COUNT_3 } from '../config/errorMap';
+import { useAnnouncer } from './useAnnouncer';
+
+const announcer = useAnnouncer();
+
+export const useConnector = createGlobalState(() => {
+ const { ws, status, data, send, open, close } = useWebSocket(urls.WS_URL, {
+ immediate: false,
+ heartbeat: {
+ interval: 3000,
+ },
+ });
+
+ watch(data, (val) => {
+ if (val === 'ping') {
+ send('pong');
+ }
+ });
+
+ const isConnecting = computed(() => status.value === 'CONNECTING');
+ const isConnected = computed(() => status.value === 'OPEN');
+
+ const time = ref({});
+ const battery = ref({});
+ const gps = ref({});
+ const position = ref({});
+ const attitude = ref({});
+ const home = ref({});
+ const homeAngle = ref(0);
+ const modeName = ref('N/A');
+ const sysStatus = ref('N/A');
+ const extra = ref({});
+
+ const info = computed(() => {
+ let result = data.value || '';
+ if (!`${data.value}`.startsWith('{')) {
+ return {};
+ }
+ try {
+ result = JSON.parse(result);
+ } catch (e) {
+ return {};
+ }
+ return result;
+ });
+
+ // 闲置计时器(闲置时做些动作)
+ let idleTimer = null;
+
+ watch(info, (val) => {
+ // info有变化时,清掉上一个计时器
+ if (idleTimer) clearTimeout(idleTimer);
+
+ // 电池信息、故障信息
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#22-%E7%B3%BB%E7%BB%9F%E7%8A%B6%E6%80%81-sys_status
+ if (val?.msgId === 1) {
+ const { VoltageBattery, CurrentBattery, BatteryRemaining, ErrorsCount1, ErrorsCount2, ErrorsCount3 } = val.data || {};
+ const voltage = +(VoltageBattery / 1e2 || 0).toFixed(1) || null; // 飞控给的电压是cV即厘伏(mavlink文档中写的是mV即毫伏)
+ const current = +(CurrentBattery / 1e2 || 0).toFixed(1) || null; // 飞控给的电压是cA即厘安
+ const remaining = BatteryRemaining >= 0 ? BatteryRemaining : null; // 单位:%
+ battery.value = { voltage, current, remaining };
+ // todo 临时
+ if (ErrorsCount1) {
+ const { [ErrorsCount1]: errorContent } = ERRORS_COUNT_1;
+ announcer.alarmThrottle(errorContent);
+ }
+ if (ErrorsCount2) {
+ const { [ErrorsCount2]: errorContent } = ERRORS_COUNT_2;
+ announcer.alarmThrottle(errorContent);
+ }
+ if (ErrorsCount3) {
+ const { [ErrorsCount3]: errorContent } = ERRORS_COUNT_3;
+ announcer.alarmThrottle(errorContent);
+ }
+ }
+
+ // 系统时间
+ // https://mavlink.io/en/messages/common.html#SYSTEM_TIME
+ if (val?.msgId === 2) {
+ const { TimeUnixUsec, TimeBootMs } = val.data || {};
+ const timestamp = parseInt(TimeUnixUsec / 1e3, 10); // to毫秒
+ const boot = TimeBootMs; // 毫秒
+ time.value = { timestamp, boot };
+ }
+
+ // GPS(with RTK)
+ // https://mavlink.io/en/messages/common.html#GPS2_RAW
+ if (val?.msgId === 124) {
+ const { FixType: fixType, SatellitesVisible: satellite } = val.data || {};
+ const fixTypeLabel = GPS_FIX_TYPE2.get(fixType);
+ gps.value = { fixType, fixTypeLabel, satellite };
+ }
+
+ // GPS(普通GPS)
+ // https://mavlink.io/en/messages/common.html#GPS_INPUT
+ if (val?.msgId === 232) {
+ if ('fixType' in gps.value) return;
+
+ const { FixType: fixType, SatellitesVisible: satellite } = val.data || {};
+ const { [fixType]: fixTypeLabel } = GPS_FIX_TYPE;
+ gps.value = { fixType, fixTypeLabel, satellite };
+ }
+
+ // 飞机位置
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#25-%E8%9E%8D%E5%90%88%E7%BB%8F%E7%BA%AC%E5%BA%A6%E9%80%9F%E5%BA%A6-global_position_int
+ if (val?.msgId === 33) {
+ const { Lon, Lat, Alt, RelativeAlt, Vx, Vy, Vz } = val.data || {};
+ const [lng, lat] = gcoord.transform([Lon / 1e7, Lat / 1e7], gcoord.WGS84, gcoord.GCJ02);
+ const alt = Alt / 1e3; // 源值毫米
+ const height = RelativeAlt / 1e3; // 源值毫米
+ const sx = (+Vx || 0) / 1e2; // 向北速度分量(源值厘米)
+ const sy = (+Vy || 0) / 1e2; // 向东速度分量(源值厘米)
+ const sz = (+Vz || 0) / 1e2; // 向下速度分量(源值厘米)
+ const hSpeed = Math.hypot(sx, sy);
+ const vSpeed = Math.abs(sz);
+
+ const { lng: hLng, lat: hLat } = home.value || {};
+ const homeDist = hLng !== undefined ? turf.distance([lng, lat], [hLng, hLat]) * 1e3 : null; // distance单位是km
+
+ position.value = { lng, lat, alt, height, hSpeed, vSpeed, sx, sy, sz, homeDist };
+ }
+
+ // 飞机姿态
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#26-%E9%A3%9E%E6%9C%BA%E5%A7%BF%E6%80%81%E8%A7%92%E9%80%9F%E5%BA%A6-attitude
+ if (val?.msgId === 30) {
+ const { Yaw, Pitch, Roll } = val.data || {};
+ const yaw = +geo.radToDeg(Yaw || 0).toFixed(1);
+ const pitch = +geo.radToDeg(Pitch || 0).toFixed(1);
+ const roll = +geo.radToDeg(Roll || 0).toFixed(1);
+ attitude.value = { yaw, pitch, roll };
+ }
+
+ // 油门
+ // https://mavlink.io/en/messages/common.html#VFR_HUD
+ if (val?.msgId === 74) {
+ const { Throttle } = val.data || {};
+ extra.value = { throttle: Throttle };
+ }
+
+ // home点
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#23-home%E7%82%B9-home_position
+ if (val?.msgId === 242) {
+ const { Longitude, Latitude } = val.data || {};
+ let lng = +(Longitude / 1e7).toFixed(7);
+ let lat = +(Latitude / 1e7).toFixed(7);
+ [lng, lat] = [lat, lng]; // todo 临时颠倒一下
+ home.value = { lng, lat };
+ }
+
+ // 飞行模式、解锁状态
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#110-%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A3%9E%E8%A1%8C%E6%A8%A1%E5%BC%8F-vkfly_custom_mode
+ // https://mavlink.io/en/messages/common.html#HEARTBEAT
+ if (val?.msgId === 0) {
+ const { CustomMode, SystemStatus } = val.data || {};
+ const { [CustomMode]: label } = FLY_MODE;
+ modeName.value = label || 'N/A';
+ sysStatus.value = FC_SYSTEM_STATUS.get(SystemStatus) || 'N/A';
+ }
+
+ // 1秒后,若info没有新的变化,则清空这些数据
+ idleTimer = setTimeout(() => {
+ time.value = {};
+ battery.value = {};
+ gps.value = {};
+ position.value = {};
+ attitude.value = {};
+ home.value = {};
+ modeName.value = 'N/A';
+ sysStatus.value = 'N/A';
+ }, 1000);
+ });
+
+ // 动态计算机头与home点的夹角
+ watch([position, attitude], () => {
+ const { lng: hLng, lat: hLat } = home.value || {};
+ if (hLng === undefined) {
+ homeAngle.value = 0;
+ return;
+ }
+ const { lng: dLng, lat: dLat } = position.value || {};
+ const { yaw } = attitude.value || {};
+ if (dLng === undefined || yaw === undefined) return;
+ const [lng, lat] = [hLng - dLng, hLat - dLat]; // 以飞机为起点,home为终点的向量
+ // 不能是0向量(飞机与home点完全重合时,则令机头始终指向home)
+ if (!lng && !lat) {
+ homeAngle.value = 0;
+ return;
+ }
+ const rad = geo.angleWithYAxis([lng, lat]); // 与(0,1)这个单位向量的夹角
+ const deg = geo.radToDeg(rad) * (lng >= 0 ? 1 : -1); // 通过x轴正负来决定角度正负
+ homeAngle.value = +(deg - yaw).toFixed(2) || 0;
+ });
+
+ return {
+ ws,
+ isConnecting,
+ isConnected,
+ time,
+ battery,
+ gps,
+ position,
+ attitude,
+ home,
+ homeAngle,
+ modeName,
+ sysStatus,
+ extra,
+ send,
+ connect: open,
+ reconnect: open,
+ close,
+ };
+});
+
+export default null;
diff --git a/src/pages/supervisionMap/useDroneMarker.js b/src/pages/supervisionMap/useDroneMarker.js
new file mode 100644
index 0000000..f50d669
--- /dev/null
+++ b/src/pages/supervisionMap/useDroneMarker.js
@@ -0,0 +1,142 @@
+import {computed, onMounted, ref, watch} from 'vue';
+import deviceIcon from "../../assets/droneImg.png";
+import droneDisImg from '../../assets/droneDisImg.png';
+import Taro from "@tarojs/taro";
+import {storeToRefs} from "pinia";
+import {useSupervisionStore} from "../../stores";
+
+
+export function useDroneMarker() {
+
+ let mapContext;
+ const { position, attitude, connectDrone, droneOnLine } = storeToRefs(useSupervisionStore());
+ const rotate = ref(0);
+
+ const path = ref([]);
+ watch(position, (nv) => {
+ if (nv?.lng) {
+ path.value.push({
+ latitude: nv?.lat,
+ longitude: nv?.lng,
+ });
+ }
+ })
+ const polyline = computed(() => {
+ if (path.value.length < 2) {
+ return [];
+ }
+ return [{
+ points: path.value || [],
+ color: '#008000',
+ width: 0.8,
+ }]
+ });
+ const markers = computed(() => {
+ if (!Object.keys(position.value).length || !mapContext) return [];
+ return [{
+ id: 1e7,
+ iconPath: droneOnLine.value ? deviceIcon : droneDisImg,
+ width: 18,
+ height: 18,
+ anchor: { x: 0.5, y: 0.5 },
+ latitude: position.value?.lat,
+ longitude: position.value?.lng,
+ rotate: attitude.value?.yaw + rotate.value,
+ }]
+ })
+
+
+ onMounted(() => {
+ mapContext = Taro.createMapContext('map');
+ })
+
+ let realTimePoint = [];
+ let lastPolylineUpdateTime = 0; // 添加最后更新时间记录
+
+ // const polyline = ref([]);
+
+ function initDevice(point, mapRotate = 0) {
+ // if (!point) {
+ // markers.value = undefined;
+ // return;
+ // }
+
+ if (!point.lng || !point.lat) {
+ markers.value = undefined;
+ return;
+ }
+ const { lng, lat, yaw } = point;
+ markers.value = {
+ ...markerConfig,
+ latitude: lat,
+ longitude: lng,
+ rotate: yaw + mapRotate,
+ };
+
+
+ realTimePoint.push(point);
+
+ // 控制轨迹线更新频率为每秒一次
+ const now = Date.now();
+ if (now - lastPolylineUpdateTime >= 1000) { // 1000ms = 1秒
+ if (realTimePoint.length >= 3) {
+ polyline.value = [{
+ points: realTimePoint.map(item => ({
+ latitude: item.lat,
+ longitude: item.lng,
+ })),
+ color: '#008000',
+ width: 0.8,
+ }];
+ }
+ lastPolylineUpdateTime = now;
+ }
+ }
+
+ function clearDevice() {
+ markers.value = undefined;
+ polyline.value = undefined;
+ realTimePoint = [];
+ lastPolylineUpdateTime = 0; // 重置最后更新时间
+ }
+ // function moveDevice(point, mapRotate = 0) {
+ // const { lng, lat, yaw } = point;
+ //
+ // map.translateMarker({
+ // markerId,
+ // destination: {
+ // longitude: lng,
+ // latitude: lat,
+ // },
+ // autoRotate: false,
+ // duration: 1,
+ // rotate: yaw + mapRotate,
+ // moveWithRotate: true,
+ // animationEnd: () => {
+ // // this._that.timelyData = { ...nextData };
+ // // this._that.currentIndex += 1;
+ // // if (this._that.isPlaying) {
+ // // this._that.elapsedMs += (duration * this._that.speedRate);
+ // // if (this._that.elapsedMs >= this._that.totalTime) {
+ // // this._that.handleStop();
+ // // return;
+ // // }
+ // // this._that._renderDevice();
+ // // }
+ // }
+ // });
+ // }
+
+ // const polyine = ref();
+ // function renderTrack(points = []) {
+ //
+ // }
+ return {
+ // initDevice,
+ // moveDevice,
+ markers,
+ clearDevice,
+ polyline,
+ rotate,
+ }
+}
diff --git a/src/stores/index.js b/src/stores/index.js
index e815b58..7942d27 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -11,4 +11,6 @@ export * from './modules/flightStore';
export * from './modules/routePlanStore';
export * from './modules/returnTripStore';
export * from './modules/evaluationStore';
-
+export * from './modules/standardStore';
+export * from './modules/supervisionStore';
+export * from './modules/airfieldsStore';
diff --git a/src/stores/modules/airfieldsStore.js b/src/stores/modules/airfieldsStore.js
new file mode 100644
index 0000000..73e4e55
--- /dev/null
+++ b/src/stores/modules/airfieldsStore.js
@@ -0,0 +1,143 @@
+/**
+ * airfieldsStore
+ */
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import http from '../../utils/http';
+import * as urls from '../../config/urls';
+import {UPDATE_AIRFIELD} from "../../config/urls";
+
+export const useAirFieldsStore = defineStore('airfields', () => {
+ // state
+ const licenseGradesList = ref([]);
+ const droneList = ref([]);
+ const classList = ref([]);
+ const airfieldList = ref([]);
+
+ const INIT_DATA = {
+ name: '',
+ circle1Lat: 0,
+ circle1Lng: 0,
+ circle2Lat: 0,
+ circle2Lng: 0,
+ envGradeId: 0,
+ licenseGradeId: 0,
+ // imageUrl: "",
+ // createDate: "",
+ // teacherId: 0,
+ // organizationId: 0,
+ }
+
+ const formData = ref({ ...INIT_DATA });
+
+ // actions
+ function getAirfieldList() {
+ return http.get(urls.GET_AIRFIELDS_LIST, {
+ params: {
+ pageNum: 1,
+ pageSize: 9999,
+ }
+ }).then(({ data: { data } = {} }) => {
+ const { records: list = [] } = data;
+ airfieldList.value = list || [];
+ return data;
+ });
+ }
+
+ function airfieldBindClass(params = {}) {
+ return http.post(urls.AIRFIELD_BIND_CLASS, params).then(({ data }) => {
+ getAirfieldList();
+ return data;
+ });
+ }
+
+ function getAirFieldsDetail(id) {
+ return http.get(urls.GET_AIRFIELDS_DETAIL(id)).then(({ data }) => {
+ return data;
+ });
+ }
+
+ function getAirFieldsOfStudent() {
+ return http.get(urls.GET_AIRFIELDS_OF_STUDENT).then(({ data }) => {
+ return data;
+ });
+ }
+
+ function deleteAirField(id) {
+ return http.delete(urls.DELETE_AIRFIELD(id)).then((data) => {
+ getAirfieldList();
+ return data;
+ })
+ }
+
+ function getDroneList() {
+ return http.get(urls.GET_DRONE_LIST, {
+ params: {
+ pageNum: 1,
+ pageSize: 9999,
+ }
+ }).then(({ data: { data } = {} }) => {
+ const { records: list = [] } = data;
+ droneList.value = list || [];
+ return data;
+ });
+ }
+
+ function getClassList() {
+ return http.get(urls.GET_CLASS_LIST, {
+ params: {
+ pageNum: 1,
+ pageSize: 9999,
+ }
+ }).then(({ data: { data } = {} }) => {
+ const { records: list = [] } = data;
+ classList.value = list || [];
+ return data;
+ });
+ }
+
+ function getLicenseGradesList() {
+ return http.get(urls.GET_LICENSE_GRADES_LIST).then(({ data: { data } = {} }) => {
+ licenseGradesList.value = data || [];
+ return data;
+ });
+ }
+
+ function createAirfield(params = {}) {
+ return http.post(urls.CREATE_AIRFIELD, params).then(({ data }) => {
+ getAirfieldList();
+ return data;
+ });
+ }
+
+ function updateAirfield(params = {}) {
+ return http.put(urls.UPDATE_AIRFIELD(params.id), params).then(({ data }) => {
+ getAirfieldList();
+ return data;
+ });
+ }
+
+ function resetFormData() {
+ formData.value = { ...INIT_DATA };
+ }
+
+
+ return {
+ getAirfieldList,
+ getAirFieldsDetail,
+ getAirFieldsOfStudent,
+ airfieldBindClass,
+ deleteAirField,
+ getDroneList,
+ droneList,
+ airfieldList,
+ getClassList,
+ classList,
+ getLicenseGradesList,
+ licenseGradesList,
+ createAirfield,
+ formData,
+ resetFormData,
+ updateAirfield,
+ };
+});
diff --git a/src/stores/modules/authStore.js b/src/stores/modules/authStore.js
index d754656..f93f150 100644
--- a/src/stores/modules/authStore.js
+++ b/src/stores/modules/authStore.js
@@ -30,6 +30,7 @@ export const useAuthStore = defineStore('auth', () => {
const requestDate = helpers.pick(formData, [
'password',
'phone',
+ 'code',
]);
return http.post(urls.LOGIN_WITH_PASSWORD, requestDate, {
@@ -56,7 +57,40 @@ export const useAuthStore = defineStore('auth', () => {
});
}
+ function studentLogin(formData = {}) {
+ const requestDate = helpers.pick(formData, [
+ 'password',
+ 'sn',
+ 'code',
+ ]);
+
+ return http.post(urls.STUDENT_LOGIN_WITH_PASSWORD, requestDate, {
+ withToken: false
+ }).then(({ data: { data } = {} }) => {
+ // const { data = {} } = data;
+ // 获取信息
+ console.log('student', data);
+
+ const { token: accessToken = '', student = {} } = data || {};
+ if (accessToken) {
+ auth.saveToken(accessToken);
+ // 更新信息
+ const newUserInfo = {
+ ...(student || {}),
+ };
+ userInfo.value = { ...newUserInfo };
+ // // 保存信息
+ UserInfo.save(newUserInfo);
+ }
+
+ return data;
+ });
+ }
+
function wechatAuth({ code, sceneId } = {}) {
+ auth.removeToken();
+ userInfo.value = {};
+ UserInfo.remove();
return http.post(urls.WECHAT_AUTH_URL, {
code,
sceneId
@@ -122,6 +156,7 @@ export const useAuthStore = defineStore('auth', () => {
return {
userInfo,
loginWithPassword,
+ studentLogin,
isSuperAdmin,
isOrgAdmin,
isTeacherAdmin,
diff --git a/src/stores/modules/flightStore.js b/src/stores/modules/flightStore.js
index 07553f7..3216981 100644
--- a/src/stores/modules/flightStore.js
+++ b/src/stores/modules/flightStore.js
@@ -12,6 +12,8 @@ export const useFlightStore = defineStore('flight', () => {
const flightExtra = ref({ total: null });
const flightQueries = ref({ pageNum: 1, pageSize: 10 });
+ const flightDetail = ref({});
+
// actions
function getFlightList(params = {}) {
const queryParams = { ...flightQueries.value, ...params };
@@ -26,6 +28,7 @@ export const useFlightStore = defineStore('flight', () => {
function getFlightDetail(id) {
return http.get(urls.GET_FLIGHT_DETAIL(id)).then(({ data }) => {
+ flightDetail.value = { ...(data?.data || {}) };
return data;
});
}
@@ -36,14 +39,35 @@ export const useFlightStore = defineStore('flight', () => {
});
}
-
+ function studentBindDrone(params = {}) {
+ return http.get(urls.STUDENT_BIND_DRONE, { params }).then(({ data: { data } = {} }) => {
+ return data;
+ });
+ }
+
+ // actions
+ function getRecordList() {
+ return http.get(urls.GET_FLIGHT_LIST, {
+ params: {
+ inProgress: true,
+ pageNum: 1,
+ pageSize: 9999,
+ }
+ }).then(({ data: { data } = {} }) => {
+ const { records: list = [] } = data;
+ return list;
+ });
+ }
return {
flightList,
flightExtra,
flightQueries,
+ flightDetail,
getFlightList,
getFlightDetail,
- getFlightTracks
+ getFlightTracks,
+ studentBindDrone,
+ getRecordList,
};
});
diff --git a/src/stores/modules/standardStore.js b/src/stores/modules/standardStore.js
new file mode 100644
index 0000000..8d76c15
--- /dev/null
+++ b/src/stores/modules/standardStore.js
@@ -0,0 +1,99 @@
+/**
+ * standardStore
+ */
+import { ref, computed } from 'vue';
+import { defineStore } from 'pinia';
+import * as urls from '../../config/urls';
+import http from '../../utils/http';
+// import * as helpers from '../../utils/helpers';
+
+export const useStandardStore = defineStore('standard', () => {
+ // state
+ const envList = ref([]);
+ const examList = ref([]);
+ const examExtra = ref({ total: null });
+ const examQueries = ref({ pageNum: 1, pageSize: 9999});
+
+ const envTidyList = computed(() => {
+ const temp = { id: 1 };
+ envList.value.forEach((item, index) => {
+ temp[item.gradeName] = {
+ index,
+ id: item.id,
+ gradeName: item.gradeName,
+ value: `${item.windMin}-${item.windMax}`,
+ originData: { ...item },
+ };
+ });
+ return [temp];
+ });
+
+ const examTidyList = computed(() => {
+ const groupedByType = {};
+
+ // 按type和name分组
+ examList.value.forEach((item) => {
+ if (!groupedByType[item.type]) {
+ groupedByType[item.type] = {};
+ }
+ const key = `${item.name}_${item.unit}`;
+ if (!groupedByType[item.type][key]) {
+ groupedByType[item.type][key] = {
+ name: item.name,
+ unit: item.unit,
+ remark: item.remark,
+ type: item.type,
+ sort: item.sort,
+ };
+ }
+
+ // 映射licenseName和envGrade到表格列号
+ let colKey;
+ if (item.licenseName === '驾驶员') {
+ colKey = item.envGrade === '一级' ? 'vlos_1' :
+ item.envGrade === '二级' ? 'vlos_2' : 'vlos_3';
+ } else if (item.licenseName === '机长') {
+ colKey = item.envGrade === '一级' ? 'bvlos_1' :
+ item.envGrade === '二级' ? 'bvlos_2' : 'bvlos_3';
+ } else if (item.licenseName === '教员') {
+ colKey = item.envGrade === '一级' ? 'instructor_1' :
+ item.envGrade === '二级' ? 'instructor_2' : 'instructor_3';
+ }
+
+ groupedByType[item.type][key][colKey] = item.value;
+ groupedByType[item.type][key][`${colKey}_data`] = { ...item };
+ });
+
+ return groupedByType;
+ });
+
+ // actions
+ function getEnvList(queries = {}) {
+ return http.get(urls.GET_ENV_LIST, { params: queries }).then(({ data: { data } = {} }) => {
+ envList.value = data || [];
+ return data;
+ });
+ }
+
+ function getExamList() {
+ const queryParams = { ...examQueries.value };
+ return http.get(urls.GET_EXAM_LIST, { params: queryParams }).then(({ data: { data } = {} }) => {
+ const { records = [], total = 0 } = data;
+ examList.value = records || [];
+ examExtra.value = { ...examExtra.value, total };
+ examQueries.value = { ...examQueries.value, ...queryParams };
+ return data;
+ });
+ }
+
+ return {
+ envList,
+ envTidyList,
+ examList,
+ examExtra,
+ examQueries,
+ examTidyList,
+ getEnvList,
+ getExamList,
+ };
+});
diff --git a/src/stores/modules/supervisionStore.js b/src/stores/modules/supervisionStore.js
index a0efe0f..a18bfd3 100644
--- a/src/stores/modules/supervisionStore.js
+++ b/src/stores/modules/supervisionStore.js
@@ -2,16 +2,237 @@
* supervisionStore
* 场地监管相关接口
*/
-import { ref } from 'vue';
+import {computed, ref, watch} from 'vue';
import { defineStore } from 'pinia';
import * as urls from '../../config/urls';
import http from '../../utils/http';
+import Taro from "@tarojs/taro";
+import gcoord from "gcoord";
+import * as turf from "@turf/turf";
+import { GPS_FIX_TYPE, GPS_FIX_TYPE2 } from '../../config/gpsFixTypeMap';
+import { FLY_MODE } from '../../config/flyModeMap';
+import { FC_SYSTEM_STATUS } from '../../config/fcSystemStatus';
+import * as geo from '../../utils/geo';
+import { TIP_TEXT } from "../../config/tipTextMap";
export const useSupervisionStore = defineStore('supervision', () => {
const fieldList = ref([]);
-
const flightQueries = ref({ pageNum: 1, pageSize: 99999, inProgress: true });
+ const connectDrone = ref(false);
+ // const disconnectDrone = ref(true);
+ const connectLoading = ref(false);
+ const time = ref({});
+ const battery = ref({});
+ const gps = ref({});
+ const position = ref({});
+ const attitude = ref({});
+ const home = ref({});
+ const homeAngle = ref(0);
+ const modeName = ref('N/A');
+ const sysStatus = ref('N/A');
+ const extra = ref({});
+ const tip = ref({});
+ const deviation = ref({});
+ const stageInfo = ref({});
+ const errorInfo = ref({});
+ const currentRecordId = ref();
+
+ const droneOnLine = computed(() => connectDrone.value && !connectLoading.value && Object.keys(position.value).length);
+
+ const data = ref();
+ const info = computed(() => {
+ let result = data.value || '';
+ if (!`${data.value}`.startsWith('{')) {
+ return {};
+ }
+ try {
+ result = JSON.parse(result);
+ } catch (e) {
+ return {};
+ }
+ return result;
+ });
+
+ // 闲置计时器(闲置时做些动作)
+ let idleTimer = null;
+
+ watch(info, (val) => {
+ // info有变化时,清掉上一个计时器
+ if (idleTimer) clearTimeout(idleTimer);
+
+ if (val?.msgType === 9) {
+ const { recordId } = val?.data || {};
+ currentRecordId.value = recordId;
+ }
+
+ if (val?.msgType === 5) {
+ const { timestamp, lat: Lat, lng: Lng, height, audioCode } = val?.data || {};
+ const [lng, lat] = gcoord.transform([Lng, Lat], gcoord.WGS84, gcoord.GCJ02);
+ errorInfo.value = {
+ timestamp,
+ lng,
+ lat,
+ height,
+ audioCode,
+ }
+ }
+
+ if (val?.msgType === 4) {
+ const { stage, timeTicker } = val?.data || {};
+ stageInfo.value = {
+ stage,
+ timeTicker: Math.floor(timeTicker / 1000),
+ }
+ }
+
+ if (val?.msgType === 3) {
+ const { speed, angle, horizontal, vertical, angleSpeed } = val?.data || {};
+ deviation.value = { speed, angle, horizontal, vertical, angleSpeed };
+ }
+
+ if (val?.msgType === 2) {
+ // const { } = val.data || {};
+ // console.log(val.data); //first_start_hints
+ // tip.value = {};
+ if (TIP_TEXT[val?.data]) {
+ tip.value = { ...TIP_TEXT[val?.data] };
+ }
+ // console.log('tip', tip.value.text, tip.value.fileUrl);
+ }
+
+ // 电池信息、故障信息
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#22-%E7%B3%BB%E7%BB%9F%E7%8A%B6%E6%80%81-sys_status
+ if (val?.msgId === 1) {
+ const { VoltageBattery, CurrentBattery, BatteryRemaining } = val.data || {};
+ const voltage = +(VoltageBattery / 1e2 || 0).toFixed(1) || null; // 飞控给的电压是cV即厘伏(mavlink文档中写的是mV即毫伏)
+ const current = +(CurrentBattery / 1e2 || 0).toFixed(1) || null; // 飞控给的电压是cA即厘安
+ const remaining = BatteryRemaining >= 0 ? BatteryRemaining : null; // 单位:%
+ battery.value = { voltage, current, remaining };
+ }
+
+ // 系统时间
+ // https://mavlink.io/en/messages/common.html#SYSTEM_TIME
+ if (val?.msgId === 2) {
+ const { TimeUnixUsec, TimeBootMs } = val.data || {};
+ const timestamp = parseInt(TimeUnixUsec / 1e3, 10); // to毫秒
+ const boot = TimeBootMs; // 毫秒
+ time.value = { timestamp, boot };
+ }
+
+ // GPS(with RTK)
+ // https://mavlink.io/en/messages/common.html#GPS2_RAW
+ if (val?.msgId === 124) {
+ const { FixType: fixType, SatellitesVisible: satellite } = val.data || {};
+ const fixTypeLabel = GPS_FIX_TYPE2.get(fixType);
+ gps.value = { fixType, fixTypeLabel, satellite };
+ }
+
+ // GPS(普通GPS)
+ // https://mavlink.io/en/messages/common.html#GPS_INPUT
+ if (val?.msgId === 232) {
+ if ('fixType' in gps.value) return;
+
+ const { FixType: fixType, SatellitesVisible: satellite } = val.data || {};
+ const { [fixType]: fixTypeLabel } = GPS_FIX_TYPE;
+ gps.value = { fixType, fixTypeLabel, satellite };
+ }
+
+ // 飞机位置
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#25-%E8%9E%8D%E5%90%88%E7%BB%8F%E7%BA%AC%E5%BA%A6%E9%80%9F%E5%BA%A6-global_position_int
+ if (val?.msgId === 33) {
+ const { Lon, Lat, Alt, RelativeAlt, Vx, Vy, Vz } = val.data || {};
+ const [lng, lat] = gcoord.transform([Lon / 1e7, Lat / 1e7], gcoord.WGS84, gcoord.GCJ02);
+ const alt = Alt / 1e3; // 源值毫米
+ const height = RelativeAlt / 1e3; // 源值毫米
+ const sy = (+Vx || 0) / 1e2; // 向北速度分量(源值厘米)
+ const sx = (+Vy || 0) / 1e2; // 向东速度分量(源值厘米)
+ const sz = (+Vz || 0) / 1e2; // 向下速度分量(源值厘米)
+ const hSpeed = Math.hypot(sx, sy);
+ const vSpeed = Math.abs(sz);
+
+ const { lng: hLng, lat: hLat } = home.value || {};
+ const homeDist = hLng !== undefined ? turf.distance([lng, lat], [hLng, hLat]) * 1e3 : null; // distance单位是km
+
+ position.value = { lng, lat, alt, height, hSpeed, vSpeed, sx, sy, sz, homeDist };
+ }
+
+ // 飞机姿态
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#26-%E9%A3%9E%E6%9C%BA%E5%A7%BF%E6%80%81%E8%A7%92%E9%80%9F%E5%BA%A6-attitude
+ if (val?.msgId === 30) {
+ const { Yaw, Pitch, Roll, Yawspeed } = val.data || {};
+ const yaw = +geo.radToDeg(Yaw || 0).toFixed(1);
+ const pitch = +geo.radToDeg(Pitch || 0).toFixed(1);
+ const roll = +geo.radToDeg(Roll || 0).toFixed(1);
+ const yawSpeed = +geo.radToDeg(Yawspeed || 0).toFixed(1);
+ attitude.value = { yaw, pitch, roll, yawSpeed };
+ }
+
+ // 油门
+ // https://mavlink.io/en/messages/common.html#VFR_HUD
+ if (val?.msgId === 74) {
+ const { Throttle } = val.data || {};
+ extra.value = { throttle: Throttle };
+ }
+
+ // home点
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#23-home%E7%82%B9-home_position
+ if (val?.msgId === 242) {
+ const { Longitude, Latitude } = val.data || {};
+ let lng = +(Longitude / 1e7).toFixed(7);
+ let lat = +(Latitude / 1e7).toFixed(7);
+ [lng, lat] = [lat, lng]; // todo 临时颠倒一下
+ home.value = { lng, lat };
+ }
+
+ // 飞行模式、解锁状态
+ // http://vk-fly.com:10880/VKFLY_INDUSTRY/VK_Mavlink/src/main#110-%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A3%9E%E8%A1%8C%E6%A8%A1%E5%BC%8F-vkfly_custom_mode
+ // https://mavlink.io/en/messages/common.html#HEARTBEAT
+ if (val?.msgId === 0) {
+ const { CustomMode, SystemStatus } = val.data || {};
+ const { [CustomMode]: label } = FLY_MODE;
+ modeName.value = label || 'N/A';
+ sysStatus.value = FC_SYSTEM_STATUS.get(SystemStatus) || 'N/A';
+ }
+
+ // 1秒后,若info没有新的变化,则清空这些数据
+ idleTimer = setTimeout(() => {
+ time.value = {};
+ battery.value = {};
+ gps.value = {};
+ position.value = {};
+ attitude.value = {};
+ home.value = {};
+ modeName.value = 'N/A';
+ sysStatus.value = 'N/A';
+ tip.value = {};
+ deviation.value = {};
+ stageInfo.value = {};
+ errorInfo.value = {};
+ }, 1000);
+ });
+
+ // 动态计算机头与home点的夹角
+ watch([position, attitude], () => {
+ const { lng: hLng, lat: hLat } = home.value || {};
+ if (hLng === undefined) {
+ homeAngle.value = 0;
+ return;
+ }
+ const { lng: dLng, lat: dLat } = position.value || {};
+ const { yaw } = attitude.value || {};
+ if (dLng === undefined || yaw === undefined) return;
+ const [lng, lat] = [hLng - dLng, hLat - dLat]; // 以飞机为起点,home为终点的向量
+ // 不能是0向量(飞机与home点完全重合时,则令机头始终指向home)
+ if (!lng && !lat) {
+ homeAngle.value = 0;
+ return;
+ }
+ const rad = geo.angleWithYAxis([lng, lat]); // 与(0,1)这个单位向量的夹角
+ const deg = geo.radToDeg(rad) * (lng >= 0 ? 1 : -1); // 通过x轴正负来决定角度正负
+ homeAngle.value = +(deg - yaw).toFixed(2) || 0;
+ });
+
// actions
function getFieldList() {
return http.get(urls.GET_FLIGHT_LIST, { params: flightQueries }).then(({ data: { data } = {} }) => {
@@ -44,8 +265,103 @@ export const useSupervisionStore = defineStore('supervision', () => {
// });
// }
+ function wsDroneData(droneId = '') {
+ if (!droneId) return;
+ let heartbeatTimer;
+ let ws;
+
+ connectLoading.value = true;
+ const heartbeat = () => {
+ if (ws && ws.readyState === 1) { // 1 = OPEN
+ ws.send({
+ data: 'ping',
+ fail: () => {}
+ });
+ }
+ };
+
+ const startHeartbeat = () => {
+ heartbeatTimer = setInterval(heartbeat, 3000); // 每30秒发送一次心跳
+ };
+
+ const stopHeartbeat = () => {
+ if (heartbeatTimer) {
+ clearInterval(heartbeatTimer);
+ heartbeatTimer = null;
+ }
+ };
+
+ return Taro.connectSocket({
+ url: urls.GET_DRONE_REAL_TIME_DATA + `?droneId=${droneId}`,
+ // success: function () {
+ // // console.log('connect success');
+ // },
+ // fail: () => {
+ // setTimeout(() => {
+ // wsDroneData(droneId);
+ // }, 3000);
+ // }
+ }).then(task => {
+ ws = task;
+
+ task.onOpen(function () {
+ connectLoading.value = false;
+ connectDrone.value = true;
+ // console.log('onOpen');
+ // task.send({ data: 'xxx' });
+ startHeartbeat(); // 连接打开后开始心跳
+ });
+
+ task.onMessage(function (msg) {
+ // console.log('onMessage: ', msg);
+ data.value = msg?.data || '';
+ // 不要在收到消息后就关闭连接
+ // task.close()
+ // setTimeout(() => {
+ // task.close();
+ // }, 5000);
+ });
+
+ task.onError(function () {
+ console.log('onError');
+ connectDrone.value = false;
+ connectLoading.value = false;
+ stopHeartbeat(); // 发生错误时停止心跳
+ // wsDroneData(droneId);
+ // task.close();
+ });
+
+ task.onClose(function (e) {
+ console.log('onClose: ', e);
+ connectDrone.value = false;
+ connectLoading.value = false;
+ stopHeartbeat(); // 连接关闭时停止心跳
+
+ // 可以在这里添加重连逻辑
+ // setTimeout(() => {
+ // wsDroneData(droneId);
+ // }, 5000); // 5秒后尝试重连
+ });
+
+ return task;
+ });
+ }
+
return {
fieldList,
getFieldList,
+ wsDroneData,
+ position,
+ attitude,
+ tip,
+ deviation,
+ connectDrone,
+ connectLoading,
+ gps,
+ errorInfo,
+ stageInfo,
+ battery,
+ currentRecordId,
+ droneOnLine,
};
});
diff --git a/src/utils/geo.js b/src/utils/geo.js
new file mode 100644
index 0000000..bf2b67c
--- /dev/null
+++ b/src/utils/geo.js
@@ -0,0 +1,118 @@
+/**
+ * 地理空间函数库
+ */
+import * as math from 'mathjs';
+import * as turf from '@turf/turf';
+
+/**
+ * 将两个点连成一个向量(并确保落在1、4象限)
+ * @param point1
+ * @param point2
+ * @returns {Vector}
+ */
+export function pointToVector(point1, point2) {
+ const vec = math.subtract(point2, point1);
+ return vec[0] < 0 ? math.multiply(-1, vec) : vec;
+}
+
+/**
+ * 弧度2角度
+ * @param radians
+ * @returns {number}
+ */
+export function radToDeg(radians) {
+ return math.multiply(radians, math.divide(180, math.pi));
+}
+
+/**
+ * 计算二维向量与x轴的夹角(弧度)
+ * @param {number[]} vector - 二维向量 [x, y]
+ * @returns {number} 夹角的弧度值
+ */
+export function angleWithXAxis(vector) {
+ const [x] = vector;
+ const magnitude = math.norm(vector);
+
+ if (magnitude === 0) {
+ throw new Error('零向量的夹角未定义');
+ }
+
+ const cosTheta = x / magnitude;
+ // 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
+ const safeCosTheta = Math.max(-1, Math.min(1, cosTheta));
+
+ return math.acos(safeCosTheta);
+}
+
+/**
+ * 计算二维向量与 y 轴的夹角(弧度)
+ * @param {number[]} vector - 二维向量 [x, y]
+ * @returns {number} 夹角的弧度值
+ */
+export function angleWithYAxis(vector) {
+ const [, y] = vector;
+ const magnitude = math.norm(vector); // 计算向量的模长 ||v||
+
+ if (magnitude === 0) {
+ throw new Error('零向量的夹角未定义');
+ }
+
+ const cosTheta = y / magnitude;
+ // 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
+ const safeCosTheta = math.max(-1, math.min(1, cosTheta));
+
+ // 计算夹角 theta
+ return math.acos(safeCosTheta);
+}
+
+/**
+ * 计算两个二维向量之间的夹角(弧度)
+ * @param {number[]} vectorA - 第一个二维向量 [x1, y1]
+ * @param {number[]} vectorB - 第二个二维向量 [x2, y2]
+ * @returns {number} 夹角的弧度值
+ */
+export function angleBetweenVectors(vectorA, vectorB) {
+ const dotProduct = math.dot(vectorA, vectorB);
+ const magnitudeA = math.norm(vectorA);
+ const magnitudeB = math.norm(vectorB);
+
+ if (magnitudeA === 0 || magnitudeB === 0) {
+ throw new Error('其中一个向量是零向量,夹角未定义');
+ }
+
+ const cosTheta = dotProduct / (magnitudeA * magnitudeB);
+ // 处理浮点数精度问题,确保 cosTheta 在 [-1, 1] 范围内
+ const safeCosTheta = Math.max(-1, Math.min(1, cosTheta));
+
+ return math.acos(safeCosTheta);
+}
+
+/**
+ * 计算两个坐标点的中点
+ * @param point1
+ * @param point2
+ * @returns {number[]}
+ */
+export function midPoint(point1, point2) {
+ const [x1, y1] = point1;
+ const [x2, y2] = point2;
+ const x = (x1 + x2) / 2;
+ const y = (y1 + y2) / 2;
+ return [+x.toFixed(8), +y.toFixed(8)];
+}
+
+/**
+ * 生成圆形路径
+ * @param center 圆心([lng, lat])
+ * @param radius 半径(单位:米)
+ * @param steps 分段数
+ * @returns {any}
+ */
+export function genCirclePath(center, radius, steps = 64) {
+ const geojson = turf.circle(center, radius / 1000, {
+ steps,
+ units: 'kilometers',
+ });
+ const [coords] = turf.getCoords(geojson);
+ return coords;
+}
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index e5798ff..2e5f4de 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -2,6 +2,7 @@
* 辅助函数
*/
import dayjs from 'dayjs';
+import gcoord from "gcoord";
/**
* 等待异步结果
@@ -117,7 +118,7 @@ export function numeric(input, substitution = '-') {
export const toFixed = (num, digits = 2) => (Number(num || 0).toFixed(digits)) - 0;
-export const falsyTo = (fv, to) => (fv || fv === 0 ? fv : to);
+export const falsyTo = (fv, to = '-') => (fv || fv === 0 ? fv : to);
/**
* 格式化时间戳
* @param timestamp
@@ -201,10 +202,18 @@ export function mostCommonElement(array) {
}, '');
}
-// /**
-// * 将gps坐标,转换为国测局坐标
-// * @param point 经纬度构成的数组[经度, 纬度]
-// */
-// export function GPS2GCJ(point) {
-// return gcoord.transform(point, gcoord.WGS84, gcoord.GCJ02);
-// }
+/**
+ * 将gps坐标,转换为国测局坐标
+ * @param point 经纬度构成的数组[经度, 纬度]
+ */
+export function GPS2GCJ(point) {
+ return gcoord.transform(point, gcoord.WGS84, gcoord.GCJ02);
+}
+
+/**
+ * 将国测局坐标,转换为gps坐标
+ * @param point 经纬度构成的数组[经度, 纬度]
+ */
+export function GCJ2GPS(point) {
+ return gcoord.transform(point, gcoord.GCJ02, gcoord.WGS84);
+}
diff --git a/src/utils/http.js b/src/utils/http.js
index 287d0b1..3efb775 100644
--- a/src/utils/http.js
+++ b/src/utils/http.js
@@ -7,6 +7,85 @@ import { getToken } from './auth';
// import * as UserInfo from "./userInfo";
// import * as Auth from "./auth";
+// // 封装登录函数
+// async function loginAndUpdateToken() {
+// try {
+// // 微信登录获取 code
+// const { code } = await Taro.login();
+//
+// // 发送 code 到服务端换取 Token
+// const res = await Taro.request({
+// url: 'your_login_api',
+// method: 'POST',
+// data: { code }
+// });
+//
+// // 存储 Token 和 Refresh Token
+// Taro.setStorageSync('access_token', res.data.access_token);
+// Taro.setStorageSync('refresh_token', res.data.refresh_token);
+// } catch (error) {
+// console.error('登录失败:', error);
+// Taro.showToast({ title: '登录失效,请重新登录', icon: 'none' });
+// Taro.navigateTo({ url: '/pages/login/index' });
+// }
+// }
+
+// // 添加全局请求拦截器
+// Taro.addInterceptor(async (chain) => {
+// const requestParams = chain.requestParams;
+//
+// // 从本地存储获取 Token
+// let token = Taro.getStorageSync('access_token');
+//
+// // 若 Token 不存在,触发登录
+// if (!token) {
+// await loginAndUpdateToken();
+// token = Taro.getStorageSync('access_token');
+// }
+//
+// // 附加 Token 到请求头
+// requestParams.header = {
+// ...requestParams.header,
+// 'Authorization': `Bearer ${token}`
+// };
+//
+// // 发送请求并处理响应
+// const response = await chain.proceed(requestParams);
+//
+// // 若返回 401 未授权错误,尝试刷新 Token
+// if (response.statusCode === 401) {
+// const newToken = await refreshToken();
+// if (newToken) {
+// Taro.setStorageSync('access_token', newToken);
+// requestParams.header.Authorization = `Bearer ${newToken}`;
+// return chain.proceed(requestParams); // 重试原请求
+// } else {
+// // 刷新失败,跳转登录页
+// Taro.navigateTo({ url: '/pages/login/index' });
+// return Promise.reject(response);
+// }
+// }
+//
+// return response;
+// });
+//
+// // 刷新 Token 的函数
+// async function refreshToken() {
+// try {
+// const res = await Taro.request({
+// url: 'your_refresh_token_endpoint',
+// method: 'POST',
+// data: {
+// refresh_token: Taro.getStorageSync('refresh_token')
+// }
+// });
+// return res.data.access_token;
+// } catch (error) {
+// console.error('刷新 Token 失败:', error);
+// return null;
+// }
+// }
+
// function judgmentHttpCode(res, reject, resolve) {
// if (res?.statusCode !== 200 || res?.data?.code !== 200) {
// reject(res?.data);