InfoModal.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. <template>
  2. <div v-drag-window class="content infoModal-content">
  3. <div class="content-title">
  4. <div class="drag-title">
  5. <span>{{ osdInfo.callsign }}</span>
  6. </div>
  7. <a class="fz16" style="color: white;" @click="() => osdInfo.visible = false">
  8. <CloseOutlined />
  9. </a>
  10. </div>
  11. <div class="content-osd">
  12. <div class="content-osd-head">
  13. <div class="content-osd-head-icon">
  14. <div class="content-osd-head-icon-image">
  15. <a-image :src="M30Src" :preview="false" />
  16. </div>
  17. <a-tooltip :title="osdInfo.model">
  18. <div class="content-osd-head-icon-text">
  19. {{ osdInfo.model }}
  20. </div>
  21. </a-tooltip>
  22. </div>
  23. <div class="content-osd-head-right">
  24. <div class="content-osd-head-right-top">
  25. <div class="content-osd-head-right-top-style">
  26. 手动飞行
  27. </div>
  28. <div class="content-osd-head-right-top-status">
  29. <div v-if="deviceInfo.device">
  30. {{ getTextByModeCode(deviceInfo.device.mode_code) }}
  31. </div>
  32. <div v-else>
  33. 当前正常
  34. </div>
  35. </div>
  36. </div>
  37. <div class="content-osd-head-right-bottom">
  38. <div class="content-osd-head-right-bottom-button">
  39. <span class="openLiveButton" @click="state.deviceLiveVisible = true" v-if="!state.deviceLiveVisible">
  40. 开启直播
  41. </span>
  42. <span class="openLiveButton" @click="state.deviceLiveVisible = false" v-else>
  43. 关闭直播
  44. </span>
  45. </div>
  46. <div class="content-osd-head-right-bottom-text">
  47. 如需切换直播请停止后重新发起
  48. </div>
  49. </div>
  50. <div class="content-osd-head-right-select">
  51. <div v-if="state.deviceLiveVisible">
  52. <a-select style="width: 125px;margin-right: 5px;" placeholder="摄像头" v-model:value="state.cameraValue">
  53. <a-select-option v-for="item in state.cameraList" :key="item.value" :value="item.value"
  54. @click="onCameraSelect(item)">
  55. {{ item.label }}
  56. </a-select-option>
  57. </a-select>
  58. <a-select style="width: 125px;margin-right: 5px;" placeholder="清晰度" v-model:value="state.clarityValue">
  59. <a-select-option v-for="item in clarityList" :key="item.value" :value="item.value">
  60. {{ item.label }}
  61. </a-select-option>
  62. </a-select>
  63. <a-tooltip title="播放">
  64. <a-button style="margin-right: 5px;" :icon="h(PlaySquareOutlined)" @click="onStartLive" />
  65. </a-tooltip>
  66. <a-tooltip title="停止">
  67. <a-button :icon="h(PoweroffOutlined)" @click="onStopLive" />
  68. </a-tooltip>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. <LivePlayer :text="state.playerText" :url="state.playerUrl" v-if="state.deviceLiveVisible" />
  74. <div class="battery-slide">
  75. <div style="background: #535759;" class="width-100"></div>
  76. <div class="capacity-percent" :style="{ width: capacity + '%' }"></div>
  77. </div>
  78. <div class="content-osd-info">
  79. <a-row style="margin-bottom: 5px;">
  80. <a-col span="6">
  81. <div style="margin-top: -3px;" class="content-osd-info-item">
  82. <a-tooltip title="遥控器信号">
  83. <div style="display: flex;align-items: flex-end;">
  84. <img style="margin-right: 5px;" :src="controllerSrc" v-if="controllerSignal > 0">
  85. <img style="margin-right: 5px;" :src="controllerErrorSrc" v-else>
  86. <div>
  87. <img :src="signalThreeSrc" v-if="controllerSignal <= 3">
  88. <img :src="signalFourSrc" v-else-if="controllerSignal === 4">
  89. <img :src="signalFiveSrc" v-else-if="controllerSignal === 5">
  90. </div>
  91. </div>
  92. </a-tooltip>
  93. <a-tooltip title="飞行器信号">
  94. <div style="display: flex;align-items: flex-end;">
  95. <img style="width: 15px;margin-right: 5px;" :src="networkSrc" v-if="aircraftSignal > 0">
  96. <img style="width: 15px;margin-right: 5px;" :src="networkErrorSrc" v-else>
  97. <div>
  98. <img :src="signalThreeSrc" v-if="aircraftSignal <= 3">
  99. <img :src="signalFourSrc" v-else-if="aircraftSignal === 4">
  100. <img :src="signalFiveSrc" v-else-if="aircraftSignal === 5">
  101. </div>
  102. </div>
  103. </a-tooltip>
  104. </div>
  105. </a-col>
  106. <a-col span="6">
  107. <div class="content-osd-info-item">
  108. <a-tooltip title="RTK">
  109. <div style="display: flex;align-items: center;margin-right: 5px;">
  110. <img style="width: 24px;margin-right: 5px;" :src="rtkSrc" v-if="RTK > 0">
  111. <img style="width: 24px; margin-right: 5px;" :src="rtkErrorSrc" v-else>
  112. <div>
  113. {{ RTK }}
  114. </div>
  115. </div>
  116. </a-tooltip>
  117. <a-tooltip title="GPS">
  118. <div style="display: flex;align-items: center;">
  119. <img style="margin-right: 5px;" :src="gpsSrc" v-if="GPS > 0">
  120. <img style="margin-right: 5px;" :src="gpsErrorSrc" v-else>
  121. <div>
  122. {{ GPS }}
  123. </div>
  124. </div>
  125. </a-tooltip>
  126. </div>
  127. </a-col>
  128. <a-col span="6">
  129. <a-tooltip title="电量">
  130. <div class="content-osd-info-item">
  131. <div style="margin-right: 5px;">
  132. <img :src="batteryOneSrc" v-if="capacity >= 75">
  133. <img :src="batteryTwoSrc" v-else-if="capacity >= 50 && capacity < 75">
  134. <img :src="batteryThreeSrc" v-else-if="capacity >= 25 && capacity < 50">
  135. <img :src="batteryFourSrc" v-else-if="capacity < 25">
  136. </div>
  137. <div>
  138. {{ capacity + '%' }}
  139. </div>
  140. </div>
  141. </a-tooltip>
  142. </a-col>
  143. <a-col span="6">
  144. <a-tooltip title="风向速度">
  145. <div class="content-osd-info-item">
  146. <img style="margin-right: 5px;" :src="windSrc">
  147. <div style="font-weight: bold;margin-right: 5px;">
  148. {{ windDirection }}
  149. </div>
  150. <div>
  151. {{ windSpeed }}
  152. </div>
  153. </div>
  154. </a-tooltip>
  155. </a-col>
  156. </a-row>
  157. <a-row>
  158. <a-col span="6">
  159. <a-tooltip title="海拔高度">
  160. <div class="content-osd-info-item">
  161. <div style="font-weight: bold;margin-right: 5px;">
  162. ASL
  163. </div>
  164. <div>
  165. {{ ASL }}
  166. </div>
  167. </div>
  168. </a-tooltip>
  169. </a-col>
  170. <a-col span="6">
  171. <a-tooltip title="离地高度">
  172. <div class="content-osd-info-item">
  173. <div style="font-weight: bold;margin-right: 5px;">
  174. AGL
  175. </div>
  176. <div>
  177. {{ AGL }}
  178. </div>
  179. </div>
  180. </a-tooltip>
  181. </a-col>
  182. <a-col span="6">
  183. <a-tooltip title="水平速度">
  184. <div class="content-osd-info-item">
  185. <div style="font-weight: bold;margin-right: 5px;">
  186. H.S
  187. </div>
  188. <div>
  189. {{ HS }}
  190. </div>
  191. </div>
  192. </a-tooltip>
  193. </a-col>
  194. <a-col span="6">
  195. <a-tooltip title="当前高度">
  196. <div class="content-osd-info-item">
  197. <img style="margin-right: 5px;" :src="homeSrc">
  198. <div>
  199. {{ homeDistance }}
  200. </div>
  201. </div>
  202. </a-tooltip>
  203. </a-col>
  204. </a-row>
  205. </div>
  206. </div>
  207. </div>
  208. </template>
  209. <script lang="ts" setup>
  210. import { h, computed, reactive, onMounted } from 'vue';
  211. import { message } from 'ant-design-vue';
  212. import { CloseOutlined, PlaySquareOutlined, PoweroffOutlined } from '@ant-design/icons-vue'
  213. import LivePlayer from '/@/components/livePlayer/index.vue';
  214. import M30Src from '../icons/info/m30.png';
  215. import controllerSrc from '../icons/info/controller.svg';
  216. import controllerErrorSrc from '../icons/info/controllerError.svg';
  217. import networkSrc from '../icons/info/network.svg';
  218. import networkErrorSrc from '../icons/info/networkError.svg';
  219. import signalThreeSrc from '../icons/info/signalThree.svg';
  220. import signalFourSrc from '../icons/info/signalFour.svg';
  221. import signalFiveSrc from '../icons/info/signalFive.svg';
  222. import rtkSrc from '../icons/info/rtk.svg';
  223. import rtkErrorSrc from '../icons/info/rtkError.svg';
  224. import gpsSrc from '../icons/info/gps.svg';
  225. import gpsErrorSrc from '../icons/info/gpsError.svg';
  226. import batteryOneSrc from '../icons/batteryOne.svg';
  227. import batteryTwoSrc from '../icons/batteryTwo.svg';
  228. import batteryThreeSrc from '../icons/batteryThree.svg';
  229. import batteryFourSrc from '../icons/batteryFour.svg';
  230. import windSrc from '../icons/info/wind.svg';
  231. import homeSrc from '../icons/info/home.svg';
  232. import { getLiveCapacity, startLivestream, stopLivestream } from '/@/api/manage';
  233. import { getTextByModeCode, getWindDirection } from '/@/utils/index'
  234. interface Props {
  235. osdInfo: any
  236. deviceInfo: any
  237. };
  238. const props = withDefaults(defineProps<Props>(), {
  239. });
  240. // 遥控器信号
  241. const controllerSignal = computed(() => {
  242. const info = props.deviceInfo?.gateway;
  243. if (info?.wireless_link) {
  244. return info.wireless_link['4g_gnd_quality'];
  245. } else {
  246. return 0;
  247. }
  248. });
  249. // 飞机信号
  250. const aircraftSignal = computed(() => {
  251. const info = props.deviceInfo?.gateway;
  252. if (info?.wireless_link) {
  253. return info.wireless_link['4g_uav_quality'];
  254. } else {
  255. return 0;
  256. }
  257. });
  258. const RTK = computed(() => {
  259. const info = props.deviceInfo?.device;
  260. if (info?.position_state) {
  261. return info.position_state.rtk_number;
  262. } else {
  263. return 0;
  264. }
  265. });
  266. const GPS = computed(() => {
  267. const info = props.deviceInfo?.device;
  268. if (info?.position_state) {
  269. return info.position_state.gps_number;
  270. } else {
  271. return 0;
  272. }
  273. });
  274. // 电池容量
  275. const capacity = computed(() => {
  276. const info = props.deviceInfo?.device;
  277. if (info?.battery) {
  278. return info.battery.capacity_percent;
  279. } else {
  280. return 0;
  281. }
  282. });
  283. const windDirection = computed(() => {
  284. const info = props.deviceInfo?.device;
  285. if (info?.wind_direction) {
  286. return getWindDirection(info.wind_direction);
  287. } else {
  288. return '';
  289. }
  290. });
  291. const windSpeed = computed(() => {
  292. const info = props.deviceInfo?.device;
  293. if (info?.wind_speed) {
  294. if (info.wind_speed === '--') {
  295. return info.wind_speed;
  296. } else {
  297. return info.wind_speed.toFixed(2) + ' m/s';
  298. }
  299. } else {
  300. return 0 + ' m/s';
  301. }
  302. });
  303. const ASL = computed(() => {
  304. const info = props.deviceInfo?.device;
  305. if (info?.height) {
  306. if (info.height === '--') {
  307. return info.height;
  308. } else {
  309. return info.height.toFixed(2) + ' m';
  310. }
  311. } else {
  312. return 0 + ' m';
  313. }
  314. });
  315. const AGL = computed(() => {
  316. const info = props.deviceInfo?.device;
  317. if (info?.elevation) {
  318. if (info.elevation === '--') {
  319. return info.elevation;
  320. } else {
  321. return info.elevation.toFixed(2) + ' m';
  322. }
  323. } else {
  324. return 0 + ' m';
  325. }
  326. });
  327. const HS = computed(() => {
  328. const info = props.deviceInfo?.device;
  329. if (info?.horizontal_speed) {
  330. if (info.horizontal_speed === '--') {
  331. return info.horizontal_speed;
  332. } else {
  333. return info.horizontal_speed.toFixed(2) + ' m/s';
  334. }
  335. } else {
  336. return 0 + ' m/s';
  337. }
  338. });
  339. const homeDistance = computed(() => {
  340. const info = props.deviceInfo?.device;
  341. if (info?.home_distance) {
  342. if (info.home_distance === '--') {
  343. return info.home_distance;
  344. } else {
  345. return info.home_distance.toFixed(2) + ' m';
  346. }
  347. } else {
  348. return 0 + ' m';
  349. }
  350. });
  351. interface SelectOption {
  352. value: any,
  353. label: string,
  354. more?: any
  355. }
  356. const clarityList: SelectOption[] = [
  357. {
  358. value: 0,
  359. label: '自适应'
  360. },
  361. {
  362. value: 1,
  363. label: '流畅'
  364. },
  365. {
  366. value: 2,
  367. label: '标清'
  368. },
  369. {
  370. value: 3,
  371. label: '高清'
  372. },
  373. {
  374. value: 4,
  375. label: '超清'
  376. }
  377. ]
  378. interface State {
  379. deviceLiveVisible: boolean,
  380. cameraList: SelectOption[],
  381. cameraValue?: string,
  382. videoList: SelectOption[],
  383. videoValue?: string,
  384. clarityValue: number,
  385. videoId: string,
  386. playerText: string,
  387. playerUrl: string,
  388. }
  389. const state: State = reactive({
  390. deviceLiveVisible: false,
  391. cameraList: [],
  392. cameraValue: undefined,
  393. videoList: [],
  394. videoValue: undefined,
  395. clarityValue: 0,
  396. videoId: '',
  397. playerText: '',
  398. playerUrl: '',
  399. })
  400. const fetchLiveCapacity = async () => {
  401. try {
  402. const res = await getLiveCapacity({});
  403. if (res.code === 0) {
  404. const deviceInfo = res.data.filter((item: any) => item.sn === props.osdInfo.sn)[0];
  405. const cameras_list = deviceInfo.cameras_list || [];
  406. const cameraList = cameras_list.map((item: any) => {
  407. return {
  408. label: item.name,
  409. value: item.index,
  410. more: item.videos_list
  411. }
  412. })
  413. state.cameraList = cameraList;
  414. }
  415. } catch (e: any) {
  416. console.error(e);
  417. }
  418. }
  419. onMounted(async () => {
  420. await fetchLiveCapacity()
  421. })
  422. const onCameraSelect = (record: SelectOption) => {
  423. state.cameraValue = record.value;
  424. if (!record.more) {
  425. return
  426. }
  427. const videoList = record.more.map((ele: any) => {
  428. return {
  429. label: ele.type,
  430. value: ele.index,
  431. more: ele.switch_video_types
  432. }
  433. })
  434. state.videoList = videoList;
  435. if (videoList.length === 0) {
  436. return;
  437. }
  438. const firstVideo: SelectOption = videoList[0];
  439. state.videoValue = firstVideo.value;
  440. }
  441. const onStartLive = async () => {
  442. const { cameraValue, videoValue } = state;
  443. if (!cameraValue) {
  444. return message.warn('请选择摄像头');
  445. }
  446. const videoId = `${props.osdInfo.sn}/${cameraValue}/${videoValue || 'normal-0'}`;
  447. state.videoId = videoId;
  448. try {
  449. const res = await startLivestream({
  450. protocol: location.protocol.slice(0, -1),
  451. video_id: videoId,
  452. url_type: 1,// RTMP
  453. video_quality: state.clarityValue
  454. });
  455. if (res.code !== 0) {
  456. state.playerText = res.message;
  457. } else {
  458. const playerUrl = res.data.url;
  459. state.playerText = '';
  460. state.playerUrl = playerUrl;
  461. message.success('已开启直播');
  462. }
  463. } catch (e: any) {
  464. console.error(e);
  465. }
  466. }
  467. const onStopLive = async () => {
  468. const { cameraValue, videoValue } = state;
  469. if (!cameraValue) {
  470. return message.warn('请选择摄像头');
  471. }
  472. const videoId = `${props.osdInfo.sn}/${cameraValue}/${videoValue || 'normal-0'}`;
  473. const res = await stopLivestream({
  474. video_id: videoId,
  475. });
  476. if (res.code === 0) {
  477. state.videoId = '';
  478. state.playerUrl = '';
  479. message.success('已停止直播');
  480. }
  481. }
  482. </script>
  483. <style lang="scss" scoped>
  484. .content {
  485. width: 420px;
  486. color: #fff;
  487. border-radius: 4px;
  488. background-color: #232323;
  489. position: absolute;
  490. left: 10px;
  491. top: 10px;
  492. &-title {
  493. width: 100%;
  494. height: 40px;
  495. padding: 5px;
  496. display: flex;
  497. justify-content: space-between;
  498. align-items: center;
  499. border: 1px solid #535759;
  500. }
  501. &-osd {
  502. font-size: 12px;
  503. &-head {
  504. display: flex;
  505. &-icon {
  506. width: 80px;
  507. padding: 5px;
  508. background: #3d3d3d;
  509. display: flex;
  510. flex-direction: column;
  511. justify-content: center;
  512. align-items: center;
  513. &-image {
  514. width: 80%;
  515. }
  516. &-text {
  517. text-align: center;
  518. white-space: nowrap;
  519. overflow: hidden;
  520. margin-bottom: 5px
  521. }
  522. }
  523. &-right {
  524. flex: 1;
  525. padding: 5px;
  526. &-top {
  527. margin: 10px 0;
  528. display: flex;
  529. align-items: center;
  530. &-style {
  531. width: 100px;
  532. color: #2a994b;
  533. border-right: 1px solid #535759;
  534. margin-right: 5px;
  535. }
  536. &-status {
  537. flex: 1;
  538. padding-left: 5px;
  539. background: #4a4d4e;
  540. white-space: nowrap;
  541. overflow: hidden;
  542. }
  543. }
  544. &-bottom {
  545. display: flex;
  546. align-items: center;
  547. &-button {
  548. width: 100px;
  549. border-right: 1px solid #535759;
  550. margin-right: 5px;
  551. .openLiveButton {
  552. padding: 2px 4px;
  553. border: 1.5px solid #535759;
  554. border-radius: 2px;
  555. cursor: pointer;
  556. }
  557. }
  558. &-text {
  559. color: #535759;
  560. }
  561. }
  562. &-select {
  563. margin: 10px 0 5px;
  564. }
  565. }
  566. }
  567. &-info {
  568. padding: 5px;
  569. &-item {
  570. display: flex;
  571. align-items: center;
  572. img {
  573. width: 13px;
  574. height: 13px;
  575. }
  576. }
  577. }
  578. }
  579. }
  580. .battery-slide {
  581. width: 100%;
  582. .capacity-percent {
  583. background: #00ee8b;
  584. }
  585. .return-home {
  586. background: #ff9f0a;
  587. }
  588. .landing {
  589. background: #f5222d;
  590. }
  591. .battery {
  592. background: white;
  593. border-radius: 1px;
  594. width: 8px;
  595. height: 4px;
  596. margin-top: -3px;
  597. }
  598. }
  599. .battery-slide>div {
  600. position: relative;
  601. margin-top: -2px;
  602. min-height: 2px;
  603. border-radius: 2px;
  604. white-space: nowrap;
  605. }
  606. </style>
  607. <style lang="scss">
  608. .infoModal-content {
  609. // 修改按钮样式
  610. .ant-btn {
  611. color: #FFFFFF;
  612. background: transparent;
  613. border: 1px solid #535759;
  614. &:hover {
  615. color: #fff;
  616. background-color: transparent;
  617. border-color: #535759;
  618. }
  619. &:focus {
  620. color: #fff;
  621. background-color: transparent;
  622. border-color: #535759;
  623. }
  624. }
  625. .ant-select-selector {
  626. color: #FFFFFF;
  627. background: transparent !important;
  628. border: 1px solid #535759 !important;
  629. box-shadow: none !important;
  630. }
  631. .ant-select-arrow {
  632. color: #fff;
  633. }
  634. .ant-select-dropdown {
  635. background-color: transparent;
  636. .ant-select-item {
  637. background-color: #000000 !important;
  638. color: #fff;
  639. &:hover {
  640. background-color: #4a4a4a !important;
  641. }
  642. }
  643. .ant-select-item-option-selected {
  644. background-color: #3a3a3a;
  645. }
  646. }
  647. }
  648. </style>