chengjunhui 3 veckor sedan
incheckning
83c55047f8

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# h5
+
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm run serve
+```
+
+### Compiles and minifies for production
+```
+npm run build
+```
+
+### Lints and fixes files
+```
+npm run lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 19 - 0
jsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  }
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 19944 - 0
package-lock.json


+ 50 - 0
package.json

@@ -0,0 +1,50 @@
+{
+  "name": "h5",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "axios": "^1.10.0",
+    "core-js": "^3.8.3",
+    "element-ui": "^2.15.14",
+    "flv.js": "^1.6.2",
+    "save": "^2.9.0",
+    "vue": "^2.6.14",
+    "vue-router": "^3.6.5",
+    "vuex": "^3.6.2",
+    "weixin-js-sdk": "^1.6.5"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "eslint": "^7.32.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "vue-template-compiler": "^2.6.14"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "@babel/eslint-parser"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 117 - 0
src/App.vue

@@ -0,0 +1,117 @@
+<template>
+  <div id="app" :style="style">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: "App",
+  data() {
+    return {
+      width: 0,
+      height: 0,
+    };
+  },
+  computed: {
+    style() {
+      return {
+        width: this.width ? this.width + "px" : "100%",
+        minHeight: this.height ? this.height + "px" : "auto",
+      };
+    },
+  },
+  created() {
+    // const width = this.getQueryParam("width") || null;
+    // const height = this.getQueryParam("height") || null;
+    // const apiToken = this.getQueryParam("apiToken") || null;
+    // const userId = this.getQueryParam("userId") || null;
+    // console.log("query=>>", this.$route.query)
+    const { width, height, apiToken, userId } = this.$route.query;
+    // console.log("apiToken", apiToken);
+    // console.log("userId=>>", userId)
+    if (userId) {
+      this.$store.commit("SET_APP_VERSION", {
+        type: "userId",
+        value: userId,
+      });
+      // localStorage.setItem("apiToken", apiToken);
+    }
+    if (apiToken) {
+      this.$store.commit("SET_APP_VERSION", {
+        type: "apiToken",
+        value: apiToken,
+      });
+      localStorage.setItem("apiToken", apiToken);
+    }
+    if (height) {
+      this.$store.commit("SET_APP_VERSION", {
+        type: "height",
+        value: height,
+      });
+      this.height = height;
+    }
+    if (width) {
+      this.$store.commit("SET_APP_VERSION", {
+        type: "width",
+        value: width,
+      });
+      this.width = width;
+    }
+  },
+  // watch: {
+  //   $route(to, from) {
+  //     // console.log("this.$route.query==>", this.$route.query);
+  //     // this.width = this.$store.state.width;
+  //     // this.height = this.$store.state.height;
+  //     console.log("from==>", from);
+  //     console.log("to==>", to);
+  //     console.log("this.$route==>", this.$route);
+  //     if (to) {
+  //       this.$nextTick(() => {
+  //         this.$router.removeRoute(from.name);
+  //       });
+  //     }
+  //   },
+  // },
+  methods: {
+    getQueryParam(paramName) {
+      const urlParams = new URLSearchParams(window.location.search);
+      return urlParams.get(paramName);
+    },
+  },
+  beforeDestroy() {
+    this.$store.dispatch("disconnect");
+  },
+};
+</script>
+
+<style>
+body {
+  display: block;
+  margin: 0 !important;
+}
+
+#app {
+  /* font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50; */
+  /* background-color: red;
+  box-sizing: border-box; */
+}
+
+.el-message {
+  min-width: auto !important;
+  background-color: rgba(88, 88, 88, 1) !important;
+  color: #fff !important;
+  border: none !important;
+}
+.el-message__content {
+  color: #fff !important;
+}
+.el-message .el-message__icon {
+  display: none !important;
+}
+</style>

+ 54 - 0
src/api/home.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request';
+
+// 获取自己的票据订单详细信息(订单编号)
+export function orderInfo(orderNo) {
+    return request({
+        url: `/api/ticket/order/detail/${orderNo}`,
+        method: 'get'
+    })
+}
+
+// 起飞
+export function flightTakeOff(data) {
+    return request({
+        url: `/prod-api/api/flight/queue/takeOff`,
+        method: 'post',
+        data
+    })
+}
+
+// 开始排队
+export function flightStart(data) {
+    return request({
+        url: `/prod-api/api/flight/queue/start`,
+        method: 'post',
+        data
+    })
+}
+
+// 取消排队
+export function flightCancelt(data) {
+    return request({
+        url: `/prod-api/api/flight/queue/cancel`,
+        method: 'post',
+        data
+    })
+}
+
+// 查看排队信息
+export function flightListAll(params) {
+    return request({
+        url: `/prod-api/api/flight/queue/list-all`,
+        method: 'get',
+        params
+    })
+}
+
+// 获取详情无人机飞行任务
+export function getDroneFlightTask(params) {
+    return request({
+        url: `/api/drone/getDroneFlightTask/${params.flightTaskUuid}`,
+        method: 'get',
+        params
+    })
+}

BIN
src/assets/back1.png


BIN
src/assets/dianliang.png


BIN
src/assets/logo.png


+ 203 - 0
src/components/LineUp.vue

@@ -0,0 +1,203 @@
+<template>
+  <div>
+    <el-drawer
+      :visible.sync="show"
+      :before-close="close"
+      direction="btt"
+      round="20px"
+      :withHeader="false"
+      size="auto"
+    >
+      <div class="lineUp">
+        <div class="title">
+          <span>排队提醒</span>
+          <div class="close" @click="close">
+             <i class="el-icon-close"></i>
+          </div>
+        </div>
+        <img
+          src="https://wrj-songlanyn.oss-cn-beijing.aliyuncs.com/static/img/lineUp.png"
+          mode=""
+          class="lineUp_img"
+        />
+        <div class="text-list">
+          <div class="text1">你购买的航线套餐无人机正在工作中</div>
+          <div class="text2">你可选择参加排队等候</div>
+          <div class="info-num-tiem">
+            <div class="info-item">
+              <span class="lable">排队人数:</span>
+              <span class="value">{{ takeOffObj.currentSumNum }}</span>
+              <span class="unit">人</span>
+            </div>
+            <div class="info-item">
+              <span class="lable">等候时长:</span>
+              <span class="value">{{ takeOffObj.waitSumTime }}</span>
+              <span class="unit">分钟</span>
+            </div>
+          </div>
+          <div class="text3">是否排队?</div>
+        </div>
+        <div class="btn-box">
+          <div class="btn-item-close" @click="close">不排队</div>
+
+          <div class="btn-item-confirm" @click="setflightStart">确认排队</div>
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { flightListAll, flightStart } from "@/api/home.js";
+export default {
+  name: "lineUp",
+  data() {
+    return {
+      show: false,
+      takeOffObj: {},
+      form: {},
+    };
+  },
+  methods: {
+    open(form) {
+      this.form = form;
+      this.getFlightListAll(form);
+      this.show = true;
+    },
+    close() {
+      this.show = false;
+      this.takeOffObj = {};
+      this.form = {};
+    },
+    // 获取排队信息
+    getFlightListAll(form) {
+      flightListAll(form).then((res) => {
+        if (res.code == 200 && res.data) {
+          this.takeOffObj = res.data;
+        }
+      });
+    },
+    setflightStart() {
+      flightStart(this.form).then((res) => {
+        console.log(res);
+        if (res.code == 200) {
+          this.$store.dispatch("initWebSocketUtil");
+          this.close();
+          this.$emit("success");
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style scoped >
+.lineUp {
+  padding: 20px 10px;
+  text-align: center;
+}
+
+.title {
+  font-size: 15px;
+  font-weight: 700;
+  position: relative;
+}
+
+.title .close {
+  position: absolute;
+  right: 5px;
+  top: -4px;
+  font-size: 20px;
+  color: #666666;
+}
+
+.lineUp_img {
+  width: 145px;
+  height: 85px;
+  margin: 20px auto 0;
+}
+
+.text1 {
+  width: 266px;
+  height: 29px;
+  line-height: 29px;
+  background: #ffe3e2;
+  border-radius: 15px;
+  font-size: 13px;
+  text-align: center;
+  color: #ff0008;
+  margin: 10px auto;
+}
+
+.text2 {
+  font-size: 13px;
+  font-weight: 700;
+  text-align: center;
+  color: #060606;
+  line-height: 18px;
+  margin-bottom: 10px;
+}
+
+.info-num-tiem {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 10px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  padding: 0 5px;
+}
+
+.lable {
+  font-size: 12px;
+  font-weight: 700;
+}
+
+.value {
+  font-size: 12px;
+  color: #ff0008;
+}
+
+.unit {
+  font-size: 12px;
+  color: #808080;
+}
+
+.text3 {
+  font-size: 12px;
+  color: #999999;
+  margin-bottom: 10px;
+}
+
+.btn-box {
+  display: flex;
+  justify-content: space-around;
+  padding: 25px 0 10px 0;
+  border-top: 1px dashed #9f9e9e;
+}
+
+.btn-box div {
+  width: 150px;
+  height: 40px;
+  line-height: 40px;
+  border-radius: 100px;
+  text-align: center;
+  color: #ffffff;
+  font-size: 14px;
+}
+
+.btn-item-close {
+  background-color: #ed8332;
+}
+
+.btn-item-confirm {
+  background-color: #fb0b03;
+}
+
+::v-deep .el-drawer{
+    border-radius: 20px 20px 0 0;
+}
+</style>

+ 237 - 0
src/components/OrderInfo.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="container">
+    <!-- 顶部导航栏 -->
+    <!-- <div class="navbar">
+      <div class="navbar-left" @click="handleBack()">
+        <img src="@/assets/back1.png" alt="" />
+      </div>
+      <div class="navbar-title">详情</div>
+    </div> -->
+    <div class="content-box">
+      <!-- 待使用状态 -->
+      <div class="topBox">
+        <div class="topBox_lab" v-if="form.orderStatus == 10">待使用</div>
+        <div
+          class="topBox_lab"
+          v-if="form.orderStatus == 20 && !form.refundStatus"
+        >
+          已完成
+        </div>
+      </div>
+
+      <!-- 订单内容 -->
+      <div class="topBox" style="padding: 0px">
+        <div class="item">
+          <!-- 商家信息 -->
+          <div class="item_t">
+            <img v-if="form.businessImage" :src="form.businessImage" />
+            <div>{{ form.businessName }}</div>
+          </div>
+
+          <!-- 商品信息 -->
+          <div class="item_c">
+            <div class="item_c_l">
+              <img
+                v-if="form.businessProductImage || form.businessImage"
+                :src="form.businessProductImage || form.businessImage"
+              />
+            </div>
+            <div class="item_c_r">
+              <div class="item_c_r_title">{{ form.businessProductName }}</div>
+              <div class="item_c_r_text" v-if="form.productDataJosn">
+                {{ form.productDataJosn.costIncluded }}
+              </div>
+              <div class="item_c_r_price">
+                <div class="item_c_r_price_r">X{{ form.buyQuantity }}</div>
+                <div class="item_c_r_price_l" v-if="form.sellingPrice">
+                  <span>{{ form.sellingPrice.split(".")[0] }}</span
+                  >.{{ form.sellingPrice.split(".")[1] }}
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 合计金额 -->
+          <div class="item_c1">
+            <div class="item_c1_lab">合计:</div>
+            <div class="item_c1_val" v-if="form.orderAmount">
+              ¥ <span>{{ String(form.orderAmount).split(".")[0] }}</span
+              >.{{ String(form.orderAmount).split(".")[1] }}
+            </div>
+          </div>
+
+          <!-- 订单基本信息 -->
+          <div class="item_c2">
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">日期:</div>
+              <div class="item_c2_item_val">
+                {{
+                  form.orderTime
+                    ? form.orderTime.substr(0, 10).replace(/-/g, ".")
+                    : "--"
+                }}
+              </div>
+            </div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">购买数量:</div>
+              <div class="item_c2_item_val">x{{ form.buyQuantity }}</div>
+            </div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">游客信息:</div>
+              <div class="item_c2_item_val" v-if="form.refundOrderId">
+                {{ form.productOrder.productVisitor.visitorName }},{{
+                  form.productOrder.productVisitor.visitorPhone
+                }}
+              </div>
+            </div>
+          </div>
+
+          <!-- 订单详细信息 -->
+          <div class="item_c2">
+            <div class="item_c2_title">订单信息</div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">订单编号:</div>
+              <div class="item_c2_item_val">{{ form.orderNumber }}</div>
+            </div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">订单创建时间:</div>
+              <div class="item_c2_item_val">
+                {{ form.orderTime ? form.orderTime.replace(/-/g, ".") : "--" }}
+              </div>
+            </div>
+            <div
+              class="item_c2_item"
+              v-if="
+                form.orderType == 0 &&
+                (form.orderStatus == 10 ||
+                  form.orderStatus == 20 ||
+                  form.refundStatus == 2 ||
+                  form.refundStatus == 1)
+              "
+            >
+              <div class="item_c2_item_lab">支付方式:</div>
+              <div class="item_c2_item_val">
+                <div v-if="form.payMethod == 'wechat'">微信支付</div>
+                <div v-if="form.payMethod == 'balance'">余额支付</div>
+              </div>
+            </div>
+            <div
+              class="item_c2_item"
+              v-if="
+                form.orderType == 0 &&
+                (form.orderStatus == 10 ||
+                  form.orderStatus == 20 ||
+                  form.refundStatus == 2 ||
+                  form.refundStatus == 1)
+              "
+            >
+              <div class="item_c2_item_lab">支付时间:</div>
+              <div class="item_c2_item_val" v-if="form.payTime">
+                {{ form.payTime ? form.payTime.replace(/-/g, ".") : "--" }}
+              </div>
+              <div class="item_c2_item_val" v-else-if="form.productOrder">
+                {{
+                  form.productOrder.payTime
+                    ? form.productOrder.payTime.replace(/-/g, ".")
+                    : "--"
+                }}
+              </div>
+              <div class="item_c2_item_val" v-else>
+                {{
+                  form.productDataJosn.payTime
+                    ? form.productDataJosn.payTime.replace(/-/g, ".")
+                    : "--"
+                }}
+              </div>
+            </div>
+          </div>
+
+          <div class="item_c2" v-if="takeOffObj.queueStatus">
+            <div class="item_c2_title">排队信息</div>
+            <template v-if="!takeOffObj.flightStatus">
+              <div class="item_c2_item">
+                <div class="item_c2_item_lab">排队人员:</div>
+                <div class="item_c2_item_val">
+                  {{ takeOffObj.currentMyNum }}
+                </div>
+              </div>
+              <div class="item_c2_item">
+                <div class="item_c2_item_lab">等候时间:</div>
+                <div class="item_c2_item_val">
+                  {{ takeOffObj.waitMyTime }}分钟
+                </div>
+              </div>
+              <div class="line_up_btn" @click="getFlightCancelt">
+                <i class="el-icon-remove-outline"></i>
+                <span class="text">取消排队</span>
+              </div>
+            </template>
+            <div class="item_c2_item_prompt" v-else>
+              <img
+                src="https://wrj-songlanyn.oss-cn-beijing.aliyuncs.com/static/img/icon_1.png"
+                mode=""
+              />
+              <div class="text">
+                请尽快操作起飞,3分钟内未起飞将取消资格,取消后需要重新操作排队
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部按钮 -->
+      <div
+        class="btnBox1"
+        v-if="
+          form.orderStatus == 10 &&
+          takeOffObj.flightStatus != null &&
+          (takeOffObj.flightStatus || !takeOffObj.queueStatus)
+        "
+      >
+        <!-- <div
+          class="btnBox_l"
+          @click="goPath('/pages/order/applicationDrawback')"
+        >
+          申请退款
+        </div> -->
+        <div
+          class="btnBox_r"
+          v-if="takeOffObj.flightStatus"
+          @click="setTakeOff()"
+        >
+          起飞拍摄
+        </div>
+        <div
+          class="btnBox_r"
+          v-if="!takeOffObj.flightStatus && !takeOffObj.queueStatus"
+          @click="lineUpRefClick()"
+        >
+          排队起飞
+        </div>
+      </div>
+    </div>
+    <LineUp ref="lineUpRef" @success="getFlightListAll()" />
+  </div>
+</template>
+
+<script>
+// import { defineComponent } from '@vue/composition-api'
+
+export default {
+    props: {
+        takeOffObj: {
+            type: Object,
+            default: () => {},
+        },
+        form: {
+            type: Object,
+            default: () => {},
+        },
+    },
+    data() {
+        return {
+            
+        }
+    },
+}
+</script>

+ 303 - 0
src/components/VideoCom.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="video-container" :style="style">
+    <div class="container-title" :style="{ width: height + 'px' }">
+      <div class="title-left" @click.stop="back()">
+        <img src="@/assets/back1.png" alt="" />
+        <div class="title-text">{{ orderInfo.businessName }}</div>
+      </div>
+      <div class="title-right">
+        <img src="@/assets/dianliang.png" alt="" />
+        <span class="percent">98%</span>
+        <span class="time">{{ currentTime }}</span>
+      </div>
+    </div>
+    <!-- 飞行数据覆盖层 -->
+    <div class="flight-data">
+      <div class="data-row">
+        <div class="data-item">D {{ horizontalDistance }}m</div>
+        <div class="data-item">H {{ altitude }}m</div>
+      </div>
+      <div class="data-row">
+        <div class="data-item">H.S {{ horizontalSpeed }}m/s</div>
+        <div class="data-item">V.S {{ verticalSpeed }}m/s</div>
+      </div>
+    </div>
+    <div class="video-box">
+      <video
+        id="videoElement"
+        controls
+        :width="height + 'px'"
+        :height="width + 'px'"
+        ref="videoRef"
+        x5-video-player-type="x5"
+      ></video>
+    </div>
+  </div>
+</template>
+
+<script>
+import flvjs from "flv.js";
+import { getDroneFlightTask, flightTakeOff } from "@/api/home";
+export default {
+  name: "App",
+  data() {
+    return {
+      liveUrl: "",
+      width: 0,
+      height: 0,
+      currentTime: "",
+      altitude: 100,
+      horizontalDistance: 0,
+      horizontalSpeed: 0,
+      verticalSpeed: 0,
+      timer: null,
+      dronePosition: {
+        lat: 39.9042,
+        lng: 116.4074,
+      },
+      flvPlayer: null,
+      flightTaskUuid: null,
+    };
+  },
+  computed: {
+    style() {
+      return {
+        width: this.width + "px",
+        height: this.height + "px",
+      };
+    },
+    orderInfo() {
+      return this.$store.state.orderInfo || {};
+    },
+  },
+  created() {
+    console.log("this.$router==>", this.$route.query);
+    this.width = this.$store.state.width;
+    this.height = this.$store.state.height;
+
+    this.$store.commit("SET_APP_VERSION", {
+      type: "currentPage",
+      value: 2,
+    });
+    
+    this.$store.commit("SET_APP_VERSION", {
+      type: "router",
+      value: this.$router,
+    });
+
+    if (this.$route.query.flightTaskUuid) {
+      this.getLiveStreamUrl();
+      // 更新时间
+      this.updateTime();
+    } else {
+      // this.$message.error("参数错误");
+      // setTimeout(() => {
+      //   this.$router.back();
+      // }, 1000);
+    }
+  },
+  mounted() {
+    // 启动定时器更新时间和模拟飞行数据
+    this.startTimeUpdate();
+    // this.simulateFlightData();
+  },
+  methods: {
+    initPlayer() {
+      if (flvjs.isSupported()) {
+        const videoElement = document.getElementById("videoElement");
+        const flvPlayer = flvjs.createPlayer({
+          type: "flv",
+          url: this.liveUrl, // 替换为你的直播流URL
+          enableStashBuffer: true, // 启用缓冲优化
+          stashInitialSize: 128, // 初始缓冲大小(单位KB)
+          isLive: true, // 表示这是一个直播流
+        });
+        // setTimeout(() => {
+        //   console.log("videoRef==>", this.$refs.videoRef);
+        // }, 1000);
+        try {
+          flvPlayer.attachMediaElement(videoElement);
+          flvPlayer.load(); // 加载视频流
+          flvPlayer.play(); // 开始播放视频流
+        } catch {}
+      } else {
+        // alert("当前浏览器不支持FLV.js");
+      }
+    },
+    getLiveStreamUrl() {
+      const loading = this.$loading({
+        lock: true,
+        text: "准备中请稍等",
+        spinner: "el-icon-loading",
+        background: "rgba(0, 0, 0, 0.7)",
+      });
+      // 实际项目中应该从API获取直播流地址
+      // 这里使用模拟数据
+      getDroneFlightTask({
+        flightTaskUuid: this.$route.query.flightTaskUuid,
+      })
+        .then((res) => {
+          loading.close();
+          if (res.code == 200 && res.data) {
+            this.info = res.data;
+            if (
+              res.data.startStreamConvert &&
+              res.data.startStreamConvert.videoUrl
+            ) {
+              this.liveUrl = res.data.startStreamConvert.videoUrl + ".flv";
+              try {
+                this.initPlayer();
+              } catch {}
+            }
+          }
+        })
+        .catch((err) => {
+          loading.close();
+          this.back();
+        });
+    },
+    getQueryParam(paramName) {
+      const urlParams = new URLSearchParams(window.location.search);
+      return urlParams.get(paramName);
+    },
+    updateTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, "0");
+      const day = String(now.getDate()).padStart(2, "0");
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      const seconds = String(now.getSeconds()).padStart(2, "0");
+      this.currentTime = `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
+    },
+    startTimeUpdate() {
+      this.timer = setInterval(() => {
+        this.updateTime();
+      }, 1000);
+    },
+    simulateFlightData() {
+      // 模拟飞行数据变化
+      setInterval(() => {
+        this.altitude = Math.floor(Math.random() * 10) + 95;
+        this.horizontalDistance = Math.floor(Math.random() * 5);
+        this.horizontalSpeed = (Math.random() * 0.5).toFixed(1);
+        this.verticalSpeed = (Math.random() * 0.5).toFixed(1);
+      }, 2000);
+    },
+    back(){
+      this.$router.push(`/?orderNumber=${this.$store.state.orderInfo.orderNumber}`);
+    }
+  },
+  beforeDestroy() {
+    // 清除定时器
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+    if (this.flvPlayer) {
+      this.flvPlayer.destroy(); // 销毁播放器实例,清理资源
+    }
+  },
+};
+</script>
+
+<style scoped>
+.video-container {
+  box-sizing: border-box;
+  overflow: hidden;
+  position: relative;
+  margin: 0 !important;
+  padding: 0;
+}
+.container-title {
+  position: absolute;
+  top: calc(50% - 20px);
+  left: -30px;
+  /* width: 100%; */
+  box-sizing: border-box;
+  background-color: rgba(0, 0, 0, 0.5);
+  padding: 10px 20px 10px 5px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  color: #ffffff;
+  font-size: 14px;
+  z-index: 20000;
+  transform: rotate(90deg);
+}
+.container-title .title-right {
+  flex-shrink: 0;
+  margin-left: 10px;
+  display: flex;
+  align-items: center;
+}
+.container-title .title-left {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+}
+
+.container-title .title-left img {
+  flex-shrink: 0;
+  width: 16px;
+  height: 16px;
+  margin-right: 5px;
+}
+
+.container-title .title-left .title-text {
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  box-sizing: border-box;
+}
+
+.container-title .title-right img {
+  width: 20px;
+  height: 20px;
+  margin-right: 5px;
+}
+
+.container-title .title-right .time {
+  margin-left: 10px;
+}
+
+.flight-data {
+  position: absolute;
+  bottom: 50%;
+  left: -25%;
+  width: 100%;
+  padding: 0;
+  box-sizing: border-box;
+  z-index: 20000;
+
+  transform: rotate(90deg);
+  color: #ffffff;
+}
+.flight-data .data-row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.flight-data .data-item {
+  min-width: 35%;
+  text-align: center;
+  margin-bottom: 10px;
+}
+.video-box {
+  transform: rotate(90deg);
+  box-sizing: border-box;
+  margin: 0;
+}
+#videoElement {
+  display: block;
+  object-fit: fill; /* 使用cover或fill */
+  margin: 0 !important;
+  z-index: 900;
+  position: relative;
+  /* position: absolute;
+  left: 0;
+  bottom: 0; */
+}
+</style>

+ 14 - 0
src/main.js

@@ -0,0 +1,14 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+Vue.use(ElementUI);
+Vue.config.productionTip = false
+
+new Vue({
+  router,
+  store,
+  render: h => h(App),
+}).$mount('#app')

+ 60 - 0
src/router/index.js

@@ -0,0 +1,60 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import DroneOperation from '../views/DroneOperation.vue'
+import OrderDetails from '../views/OrderDetails.vue'
+
+Vue.use(VueRouter)
+
+const routes = [
+  // {
+  //   path: '',
+  //   // name: 'OrderDetails',
+  //   // component: OrderDetails,
+  //   // meta: { requiresAuth: true },
+  //   redirect: '/',
+  // },
+  {
+    path: '/',
+    name: 'OrderDetails',
+    component: OrderDetails,
+    meta: { requiresAuth: true },
+  },
+  {
+    path: '/droneOperation',
+    name: 'DroneOperation',
+    component: DroneOperation,
+    meta: { requiresAuth: true }
+  }
+]
+
+const router = new VueRouter({
+  mode: 'hash',
+  base: process.env.BASE_URL,
+  routes
+})
+
+// 导航守卫
+router.beforeEach((to, from, next) => {
+  next();
+  // // 检查路由是否需要身份验证
+  // if (to.matched.some(record => record.meta.requiresAuth)) {
+  //   // 检查用户是否已登录
+  //   const isLoggedIn = localStorage.getItem('droneLoggedIn') === 'true';
+    
+  //   if (!isLoggedIn) {
+  //     // 如果未登录,重定向到登录页面
+  //     next({
+  //       path: '/login',
+  //       query: { redirect: to.fullPath } // 保存原本要访问的路径
+  //     });
+  //   } else {
+  //     // 如果已登录,正常进入
+  //     next();
+  //   }
+  // } else {
+  //   // 不需要身份验证的路由,正常进入
+  //   next();
+  // }
+});
+
+export default router

+ 182 - 0
src/store/index.js

@@ -0,0 +1,182 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import WebSocketUtil from '@/utils/WebSocketUtil';
+// import Router from '@/router'
+import { Message, MessageBox } from 'element-ui';
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {
+    status: 0, // 连接状态:0-未连接,1-连接中,2-已连接
+    // 全局状态
+    width: 0,
+    height: 0,
+    apiToken: "",
+    webSocket: WebSocketUtil,
+    wsUrl: "ws://192.168.0.151:9002/socket/flight",
+    userId: '',  // 用户名称
+    orderInfo: null, // 订单信息
+    flightTaskUuid: '', // 无人机飞行任务id
+    loading: null,  // loading
+    taskNum: 0,  // 重新创建任务次数
+    takeOffShow: false,  // 是否可以起飞
+    flightData: {
+
+    },  // 无人机飞行数据
+    currentPage: 1, // 1: 订单详情页,2:无人机详情页
+    router: null,  // 路由数据
+
+  },
+  mutations: {
+    // 全局状态修改方法
+    SET_APP_VERSION(state, version) {
+      // console.log(version)
+      state[version.type] = version.value;
+      // console.log(this.state.status)
+      // console.log('初始化WebSocket', this.state.orderInfo.businessProductId)
+
+    }
+  },
+  actions: {
+    // 全局异步操作
+    updateAppVersion({ commit }, value) {
+      this.apiToken = value
+    },
+    // 初始化WebSocket
+    initWebSocketUtil({ commit }, value) {
+      console.log('初始化WebSocket', this.$router)
+      // console.log('初始化WebSocket', this.state.orderInfo)
+      // return
+      if (this.state.status != 0) return;
+      if (!this.state.orderInfo) return;
+      if (!this.state.userId) return;
+      // console.log('11111');
+      this.state.status = 1; // 标记为连接中
+      const url = `${this.state.wsUrl}/${this.state.orderInfo.businessProductId}/${this.state.userId}`
+      console.log('初始化WebSocket_url', url)
+      // 初始化WebSocket
+      this.state.webSocket.init({
+        url,
+        heartbeatInterval: 5000, // 10秒发送一次心跳,方便演示
+        reconnectInterval: 3000, // 3秒重连一次
+        reconnectMaxTimes: 5 // 最多重连5次
+      });
+      this.dispatch('addWebSocketListeners');
+    },
+    // 添加WebSocket事件监听
+    addWebSocketListeners() {
+      // console.log('添加WebSocket事件监听');
+      // 连接打开事件
+      this.state.webSocket.addEventListener('open', (event) => {
+        this.state.status = 2;
+        // console.log('WebSocket连接已打开');
+        // this.addMessage('system', '连接已建立');
+      });
+
+      // 消息接收事件
+      this.state.webSocket.addEventListener('message', ({ data }) => {
+        console.log('收到消息:', data);
+        // 如果是心跳消息
+        if (data && data.type === 'heartbeat') {
+          // this.lastHeartbeat = this.formatTime(new Date());
+          // this.addMessage('heartbeat', `收到心跳响应: ${JSON.stringify(data)}`);
+          return;
+        }
+        // 排队完成可以起飞 
+        if (data.msgType == 1) {
+          // if (this.state.loading) this.state.loading.close();
+          // this.state.loading = null;
+          // Router.push(`/droneOperation?flightTaskUuid=${this.state.flightTaskUuid}`);
+          this.state.takeOffShow = true;
+        }
+        // 飞行结束
+        if (data.msgType == 2) {
+          Message.success('飞行结束,即将退出');
+          setTimeout(() => {
+            this.state.router.replace(`/?orderNumber=${this.state.orderInfo.orderNumber}`);
+          }, 1000);
+          this.dispatch('disconnect');
+        }
+        // 创建任务失败,是否重新创建
+        if (data.msgType == 3) {
+          console.log('当前页面', this.state.currentPage);
+          if (this.state.currentPage == 2) {
+            // Message.error('飞行任务创建失败');
+            setTimeout(() => {
+              console.log('this.state.router===>', this.state.router);
+              console.log('window.history===>', window.history.length);
+              console.log('router.getRoutes===>', this.state.router.getRoutes());
+              try {
+                if (window.history.length > 1) {
+                  // this.state.router.go(-1);
+                  // this.state.router.back();
+                  // this.state.router.replace('/droneOperation')
+                  // window.location.back();
+                  this.state.router.replace(`/?orderNumber=${this.state.orderInfo.orderNumber}`);
+                } else {
+                  this.state.router.replace(`/?orderNumber=${this.state.orderInfo.orderNumber}`);
+                }
+              } catch (e) {
+                console.log(e);
+              }
+            }, 2000);
+            return
+          }
+          MessageBox.confirm('飞行任务创建失败,是否重新创建飞行任务?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }).then(() => {
+            this.state.taskNum++;
+          }).catch(() => {
+            this.state.taskNum = 0;
+            if (this.state.currentPage == 2) {
+              // this.state.router.back();
+              this.state.router.replace(`/?orderNumber=${this.state.orderInfo.orderNumber}`);
+            }
+          });
+        }
+        // 飞行任务创建结束,可以起飞
+        if (data.msgType == 4) {
+          this.state.taskNum = 0;
+          if (this.state.loading) this.state.loading.close();
+          this.state.loading = null;
+          // Message.success('飞行任务创建成功,即将起飞');
+          // this.state.router.replace(`/droneOperation?flightTaskUuid=${this.state.flightTaskUuid}&t=${new Date().getTime()}`);
+          console.log("flightTaskUuid===》", this.state.flightTaskUuid);
+          console.log("跳转页面vuex====>")
+          this.state.router.push(`/droneOperation?t=${new Date().getTime()}&flightTaskUuid=${this.state.flightTaskUuid}`);
+        }
+
+        if (data.msgType == 10) {
+
+        }
+
+        // 普通消息
+        // this.addMessage('received', typeof data === 'object' ? JSON.stringify(data) : data);
+      });
+
+      // 错误事件
+      this.state.webSocket.addEventListener('error', (event) => {
+        this.status = 0;
+        console.log('WebSocket连接错误:', event);
+      });
+
+      // 关闭事件
+      this.state.webSocket.addEventListener('close', (event) => {
+        this.state.status = 0;
+        console.log('WebSocket连接关闭:', event);
+      });
+    },
+    // 断开WebSocket连接
+    disconnect() {
+      if (this.state.status == 0) return;
+      this.state.webSocket.close();
+      this.state.status = 0;
+    },
+  },
+  modules: {
+
+
+  }
+})

+ 167 - 0
src/utils/WebSocketUtil.js

@@ -0,0 +1,167 @@
+/**
+ * WebSocket工具类封装
+ * 提供更简单的API接口,方便业务层使用
+ */
+
+import WebSocketClient from './websocket';
+
+// WebSocket实例
+let wsInstance = null;
+
+// 事件监听器集合
+const eventListeners = {};
+
+/**
+ * WebSocket工具类
+ */
+const WebSocketUtil = {
+  /**
+   * 初始化WebSocket连接
+   * @param {Object} options 配置项
+   * @param {String} options.url WebSocket连接地址
+   * @param {Number} options.heartbeatInterval 心跳间隔时间(ms),默认30000ms
+   * @param {Number} options.reconnectInterval 重连间隔时间(ms),默认5000ms
+   * @param {Number} options.reconnectMaxTimes 最大重连次数,默认10次
+   */
+  init(options) {
+    if (wsInstance) {
+      console.warn('WebSocket已初始化,请勿重复初始化');
+      return;
+    }
+    
+    wsInstance = new WebSocketClient({
+      ...options,
+      onOpen: (event) => {
+        // console.log('WebSocket连接成功');
+        this.dispatchEvent('open', event);
+      },
+      onMessage: (data, event) => {
+        console.log('WebSocket收到消息:', data);
+        this.dispatchEvent('message', { data, event });
+      },
+      onError: (event) => {
+        // console.error('WebSocket连接错误:', event);
+        this.dispatchEvent('error', event);
+      },
+      onClose: (event) => {
+        // console.log('WebSocket连接关闭:', event);
+        this.dispatchEvent('close', event);
+      }
+    });
+  },
+  
+  /**
+   * 连接WebSocket
+   * @param {String} url 连接地址,可选,如果不传则使用初始化时的url
+   */
+  connect(url) {
+    if (!wsInstance) {
+      if (!url) {
+        console.log('WebSocket未初始化,请先调用init方法');
+        return;
+      }
+      
+      // 如果未初始化,则先初始化
+      this.init({ url });
+      return;
+    }
+    
+    wsInstance.connect(url);
+  },
+  
+  /**
+   * 发送消息
+   * @param {Object|String} data 要发送的数据
+   * @returns {Boolean} 是否发送成功
+   */
+  send(data) {
+    if (!wsInstance) {
+      console.log('WebSocket未初始化,请先调用init方法');
+      return false;
+    }
+    
+    return wsInstance.send(data);
+  },
+  
+  /**
+   * 关闭连接
+   */
+  close() {
+    if (!wsInstance) {
+      console.warn('WebSocket未初始化,无需关闭');
+      return;
+    }
+    
+    wsInstance.close();
+    wsInstance = null;
+  },
+  
+  /**
+   * 获取当前连接状态
+   * @returns {Number} 0-未连接,1-连接中,2-已连接
+   */
+  getStatus() {
+    if (!wsInstance) {
+      return 0;
+    }
+    
+    return wsInstance.getStatus();
+  },
+  
+  /**
+   * 添加事件监听
+   * @param {String} event 事件名称,支持:open, message, error, close
+   * @param {Function} callback 回调函数
+   */
+  addEventListener(event, callback) {
+    if (!eventListeners[event]) {
+      eventListeners[event] = [];
+    }
+    
+    eventListeners[event].push(callback);
+  },
+  
+  /**
+   * 移除事件监听
+   * @param {String} event 事件名称
+   * @param {Function} callback 回调函数,如果不传则移除该事件的所有监听
+   */
+  removeEventListener(event, callback) {
+    if (!eventListeners[event]) {
+      return;
+    }
+    
+    if (!callback) {
+      // 移除该事件的所有监听
+      eventListeners[event] = [];
+      return;
+    }
+    
+    // 移除指定回调
+    const index = eventListeners[event].indexOf(callback);
+    if (index !== -1) {
+      eventListeners[event].splice(index, 1);
+    }
+  },
+  
+  /**
+   * 触发事件
+   * @param {String} event 事件名称
+   * @param {*} data 事件数据
+   */
+  dispatchEvent(event, data) {
+    if (!eventListeners[event]) {
+      return;
+    }
+    
+    eventListeners[event].forEach(callback => {
+      try {
+        callback(data);
+      } catch (error) {
+        console.log(`WebSocket事件${event}回调执行错误:`, error);
+      }
+    });
+  }
+};
+
+export default WebSocketUtil;

+ 6 - 0
src/utils/errorCode.js

@@ -0,0 +1,6 @@
+export default {
+  '401': '认证失败,无法访问系统资源',
+  '403': '当前操作没有权限',
+  '404': '访问资源不存在',
+  'default': '系统未知错误,请反馈给管理员'
+}

+ 25 - 0
src/utils/index.js

@@ -0,0 +1,25 @@
+/**
+* 参数处理
+* @param {*} params  参数
+*/
+export function tansParams(params) {
+  let result = ''
+  for (const propName of Object.keys(params)) {
+    const value = params[propName];
+    var part = encodeURIComponent(propName) + "=";
+    if (value !== null && typeof (value) !== "undefined") {
+      if (typeof value === 'object') {
+        for (const key of Object.keys(value)) {
+          if (value[key] !== null && typeof (value[key]) !== 'undefined') {
+            let params = propName + '[' + key + ']';
+            var subPart = encodeURIComponent(params) + "=";
+            result += subPart + encodeURIComponent(value[key]) + "&";
+          }
+        }
+      } else {
+        result += part + encodeURIComponent(value) + "&";
+      }
+    }
+  }
+  return result
+}

+ 100 - 0
src/utils/request.js

@@ -0,0 +1,100 @@
+import axios from 'axios'
+import { Notification, MessageBox, Message, Loading } from 'element-ui'
+import errorCode from '@/utils/errorCode'
+import { tansParams } from "@/utils/index";
+import wx from "weixin-js-sdk";
+
+let downloadLoadingInstance;
+// 是否显示重新登录
+export let isRelogin = { show: false };
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+
+// 创建axios实例
+const service = axios.create({
+  // axios中请求配置有baseURL选项,表示请求URL公共部分
+  baseURL: '',
+  // 超时
+  timeout: 1000 * 60 * 3
+})
+
+// request拦截器
+service.interceptors.request.use(config => {
+  // 是否需要设置 token
+  const isToken = (config.headers || {}).isToken === false
+  // 是否需要防止数据重复提交
+  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
+  const apiToken = localStorage.getItem("apiToken")
+  if (apiToken) {
+    config.headers['apiToken'] = apiToken // 让每个请求携带自定义token 请根据实际情况自行修改
+  }
+
+  // get请求映射params参数
+  if (config.method === 'get' && config.params) {
+    let url = config.url + '?' + tansParams(config.params);
+    url = url.slice(0, -1);
+    config.params = {};
+    config.url = url;
+  }
+
+  return config
+}, error => {
+  console.log(error)
+  Promise.reject(error)
+})
+
+// 响应拦截器
+service.interceptors.response.use(res => {
+  // 未设置状态码则默认成功状态
+  const code = res.data.code || 200;
+  // 获取错误信息
+  const msg = errorCode[code] || res.data.msg || errorCode['default']
+  // 二进制数据则直接返回
+  if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+    return res.data
+  }
+
+  if (code === 500) {
+    Message({
+      message: msg,
+      type: 'error',
+      duration: 5 * 1000
+    })
+    return Promise.reject(msg)
+  } else if (code !== 200) {
+    Notification.error({
+      title: msg
+    })
+    // 判断是否需要返回到登录页面重新登录
+    if ([401].includes(code)) {
+      wx.miniProgram.reLaunch({
+        url: `/pages/login/index`
+      })
+    }
+    return Promise.reject('error')
+  } else {
+    return res.data
+  }
+},
+  error => {
+    console.log('err' + error)
+    let { message } = error;
+    if (message == "Network Error") {
+      message = "后端接口连接异常";
+    }
+    else if (message.includes("timeout")) {
+      message = "系统接口请求超时";
+    }
+    else if (message.includes("Request failed with status code")) {
+      message = "系统接口" + message.substr(message.length - 3) + "异常";
+    }
+    Message({
+      message: message,
+      type: 'error',
+      duration: 5 * 1000
+    })
+    return Promise.reject(error)
+  }
+)
+
+export default service

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
src/utils/uni.webview.1.5.4.js


+ 137 - 0
src/utils/websocket-readme.md

@@ -0,0 +1,137 @@
+# WebSocket工具类使用说明
+
+## 简介
+
+本工具类提供了WebSocket连接管理、自动重连、心跳检测等功能,可以方便地在Vue项目中使用WebSocket进行实时通信。
+
+## 文件结构
+
+- `websocket.js`: WebSocket核心实现类,提供WebSocket连接管理、自动重连、心跳检测等功能
+- `WebSocketUtil.js`: WebSocket工具类封装,提供更简单的API接口,方便业务层使用
+- `WebSocketDemo.vue`: WebSocket使用示例组件
+
+## 功能特性
+
+1. **连接管理**:自动建立连接、关闭连接
+2. **自动重连**:连接断开后自动重连,可配置重连间隔和最大重连次数
+3. **心跳检测**:定时发送心跳消息,保持连接活跃
+4. **事件监听**:支持监听连接打开、消息接收、错误、关闭等事件
+5. **消息发送**:支持发送文本消息和JSON对象
+
+## 使用方法
+
+### 1. 初始化WebSocket
+
+```javascript
+import WebSocketUtil from '@/utils/WebSocketUtil';
+
+// 初始化WebSocket
+WebSocketUtil.init({
+  url: 'ws://your-websocket-server.com',  // WebSocket服务地址
+  heartbeatInterval: 30000,               // 心跳间隔,默认30秒
+  reconnectInterval: 5000,                // 重连间隔,默认5秒
+  reconnectMaxTimes: 10                   // 最大重连次数,默认10次
+});
+```
+
+### 2. 添加事件监听
+
+```javascript
+// 连接打开事件
+WebSocketUtil.addEventListener('open', (event) => {
+  console.log('WebSocket连接已建立');
+});
+
+// 消息接收事件
+WebSocketUtil.addEventListener('message', ({ data }) => {
+  console.log('收到消息:', data);
+});
+
+// 错误事件
+WebSocketUtil.addEventListener('error', (event) => {
+  console.log('WebSocket连接错误');
+});
+
+// 关闭事件
+WebSocketUtil.addEventListener('close', (event) => {
+  console.log('WebSocket连接已关闭');
+});
+```
+
+### 3. 发送消息
+
+```javascript
+// 发送文本消息
+WebSocketUtil.send('Hello, WebSocket!');
+
+// 发送JSON对象
+WebSocketUtil.send({
+  type: 'chat',
+  content: '这是一条消息',
+  timestamp: new Date().getTime()
+});
+```
+
+### 4. 关闭连接
+
+```javascript
+WebSocketUtil.close();
+```
+
+### 5. 获取连接状态
+
+```javascript
+const status = WebSocketUtil.getStatus();
+// 0: 未连接, 1: 连接中, 2: 已连接
+```
+
+## 在Vue组件中使用
+
+在Vue组件中使用WebSocket时,建议在组件创建时添加事件监听,在组件销毁前关闭连接:
+
+```javascript
+export default {
+  created() {
+    // 初始化WebSocket
+    WebSocketUtil.init({
+      url: 'ws://your-websocket-server.com'
+    });
+    
+    // 添加事件监听
+    WebSocketUtil.addEventListener('message', this.handleMessage);
+  },
+  beforeDestroy() {
+    // 移除事件监听
+    WebSocketUtil.removeEventListener('message', this.handleMessage);
+    
+    // 关闭连接
+    WebSocketUtil.close();
+  },
+  methods: {
+    handleMessage({ data }) {
+      // 处理接收到的消息
+      console.log('收到消息:', data);
+    }
+  }
+};
+```
+
+## 示例组件
+
+项目中提供了一个WebSocketDemo组件,可以通过访问`/websocket-demo`路由查看示例效果。
+
+示例组件展示了如何在Vue组件中使用WebSocketUtil,包括:
+
+1. 连接/断开WebSocket
+2. 发送消息
+3. 接收消息
+4. 心跳检测
+5. 状态显示
+
+## 注意事项
+
+1. WebSocket连接需要服务器支持,确保服务器已正确配置WebSocket
+2. 心跳间隔不宜过短,建议30秒以上
+3. 在组件销毁前务必关闭WebSocket连接,避免内存泄漏
+4. 处理接收到的消息时,注意判断消息类型,避免错误处理
+5. 在移动设备上,网络切换可能导致WebSocket连接断开,此时会自动重连

+ 281 - 0
src/utils/websocket.js

@@ -0,0 +1,281 @@
+/**
+ * WebSocket工具类
+ * 功能:
+ * 1. WebSocket连接管理
+ * 2. 自动重连机制
+ * 3. 心跳检测机制
+ * 4. 消息发送和接收
+ * 5. 事件监听和回调
+ */
+
+class WebSocketClient {
+  /**
+   * 构造函数
+   * @param {Object} options 配置项
+   * @param {String} options.url WebSocket连接地址
+   * @param {Number} options.heartbeatInterval 心跳间隔时间(ms),默认30000ms
+   * @param {Number} options.reconnectInterval 重连间隔时间(ms),默认5000ms
+   * @param {Number} options.reconnectMaxTimes 最大重连次数,默认10次
+   * @param {Function} options.onOpen 连接成功回调
+   * @param {Function} options.onMessage 消息接收回调
+   * @param {Function} options.onError 错误回调
+   * @param {Function} options.onClose 连接关闭回调
+   */
+  constructor(options) {
+    this.url = options.url || '';
+    this.heartbeatInterval = options.heartbeatInterval || 30000; // 默认30s发送一次心跳
+    this.reconnectInterval = options.reconnectInterval || 5000; // 默认5s重连一次
+    this.reconnectMaxTimes = options.reconnectMaxTimes || 10; // 默认最多重连10次
+    this.reconnectCount = 0; // 当前重连次数
+    this.heartbeatTimer = null; // 心跳定时器
+    this.reconnectTimer = null; // 重连定时器
+    this.websocket = null; // WebSocket实例
+    this.status = 0; // 连接状态:0-未连接,1-连接中,2-已连接
+    this.onOpen = options.onOpen || function() {};
+    this.onMessage = options.onMessage || function() {};
+    this.onError = options.onError || function() {};
+    this.onClose = options.onClose || function() {};
+    
+    // 初始化连接
+    if (this.url) {
+      this.connect();
+    }
+  }
+
+  /**
+   * 连接WebSocket
+   * @param {String} url 连接地址,可选,如果不传则使用构造函数中的url
+   */
+  connect(url) {
+    if (url) {
+      this.url = url;
+    }
+    
+    if (!this.url) {
+      console.log('WebSocket连接地址不能为空');
+      return;
+    }
+    
+    // 防止重复连接
+    if (this.status === 1) {
+      console.log('WebSocket正在连接中,请勿重复连接');
+      return;
+    }
+    
+    // 如果已经连接,先关闭
+    if (this.websocket) {
+      this.close();
+    }
+    
+    this.status = 1; // 标记为连接中
+    
+    try {
+      this.websocket = new WebSocket(this.url);
+      
+      // 监听连接打开事件
+      this.websocket.onopen = (event) => {
+        this.status = 2; // 标记为已连接
+        this.reconnectCount = 0; // 重置重连次数
+        console.log('WebSocket连接成功');
+        
+        // 开启心跳检测
+        this.heartbeat();
+        
+        // 执行外部回调
+        this.onOpen(event);
+      };
+      
+      // 监听消息接收事件
+      this.websocket.onmessage = (event) => {
+        // 收到任何消息都视为连接正常,重置心跳定时器
+        this.resetHeartbeat();
+        
+        // 处理消息
+        let data = event.data;
+        try {
+          // 尝试解析JSON
+          data = JSON.parse(data);
+          
+          // 如果是心跳响应,不触发外部回调
+          if (data.type === 'heartbeat') {
+            console.log('收到心跳响应:', data);
+            return;
+          }
+        } catch (e) {
+          // 非JSON格式,保持原样
+        }
+        
+        // 执行外部回调
+        this.onMessage(data, event);
+      };
+      
+      // 监听错误事件
+      this.websocket.onerror = (event) => {
+        console.log('WebSocket连接错误:', event);
+        this.status = 0; // 标记为未连接
+        
+        // 执行外部回调
+        this.onError(event);
+        
+        // 尝试重连
+        this.reconnect();
+      };
+      
+      // 监听关闭事件
+      this.websocket.onclose = (event) => {
+        console.log('WebSocket连接关闭:', event);
+        this.status = 0; // 标记为未连接
+        
+        // 清除心跳定时器
+        this.clearHeartbeat();
+        
+        // 执行外部回调
+        this.onClose(event);
+        
+        // 如果不是主动关闭,则尝试重连
+        if (event.code !== 1000) {
+          this.reconnect();
+        }
+      };
+    } catch (error) {
+      console.log('WebSocket连接失败:', error);
+      this.status = 0; // 标记为未连接
+      
+      // 尝试重连
+      this.reconnect();
+    }
+  }
+
+  /**
+   * 重连
+   */
+  reconnect() {
+    // 已达到最大重连次数
+    if (this.reconnectCount >= this.reconnectMaxTimes) {
+      console.warn(`WebSocket已达到最大重连次数${this.reconnectMaxTimes}次,停止重连`);
+      return;
+    }
+    
+    // 防止重复启动重连定时器
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+    }
+    
+    this.reconnectTimer = setTimeout(() => {
+      console.log(`WebSocket第${this.reconnectCount + 1}次重连...`);
+      this.reconnectCount++;
+      this.connect(); // 重新连接
+    }, this.reconnectInterval);
+  }
+
+  /**
+   * 发送心跳
+   */
+  heartbeat() {
+    // 清除之前的心跳定时器
+    this.clearHeartbeat();
+    
+    // 创建新的心跳定时器
+    this.heartbeatTimer = setInterval(() => {
+      if (this.status === 2) {
+        // 发送心跳消息
+        this.send({
+          type: 'heartbeat',
+          data: 'ping',
+          timestamp: new Date().getTime()
+        });
+        console.log('发送心跳消息');
+      } else {
+        // 如果连接已断开,清除心跳定时器
+        this.clearHeartbeat();
+      }
+    }, this.heartbeatInterval);
+  }
+
+  /**
+   * 重置心跳
+   * 收到消息后调用,重置心跳定时器
+   */
+  resetHeartbeat() {
+    this.clearHeartbeat();
+    this.heartbeat();
+  }
+
+  /**
+   * 清除心跳定时器
+   */
+  clearHeartbeat() {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer);
+      this.heartbeatTimer = null;
+    }
+  }
+
+  /**
+   * 发送消息
+   * @param {Object|String} data 要发送的数据
+   * @returns {Boolean} 是否发送成功
+   */
+  send(data) {
+    if (this.status !== 2) {
+      console.log('WebSocket未连接,无法发送消息');
+      return false;
+    }
+    
+    try {
+      // 如果是对象,转为JSON字符串
+      if (typeof data === 'object') {
+        data = JSON.stringify(data);
+      }
+      
+      this.websocket.send(data);
+      return true;
+    } catch (error) {
+      console.log('WebSocket发送消息失败:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 关闭连接
+   */
+  close() {
+    // 清除定时器
+    this.clearHeartbeat();
+    
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+    }
+    
+    // 关闭WebSocket连接
+    if (this.websocket) {
+      // 移除所有事件监听
+      this.websocket.onopen = null;
+      this.websocket.onmessage = null;
+      this.websocket.onerror = null;
+      this.websocket.onclose = null;
+      
+      // 如果连接已打开,则关闭
+      if (this.status === 2) {
+        this.websocket.close(1000, '主动关闭');
+      }
+      
+      this.websocket = null;
+    }
+    
+    this.status = 0; // 标记为未连接
+  }
+
+  /**
+   * 获取当前连接状态
+   * @returns {Number} 0-未连接,1-连接中,2-已连接
+   */
+  getStatus() {
+    return this.status;
+  }
+}
+
+// 导出WebSocket工具类
+export default WebSocketClient;

+ 338 - 0
src/views/DroneOperation.vue

@@ -0,0 +1,338 @@
+<template>
+  <div class="video-container" :style="style">
+    <div class="container-title" :style="{ width: height + 'px' }">
+      <div class="title-left" @click.stop="back()">
+        <img src="@/assets/back1.png" alt="" />
+        <div class="title-text">{{ orderInfo.businessName }}</div>
+      </div>
+      <div class="title-right">
+        <img src="@/assets/dianliang.png" alt="" />
+        <span class="percent">98%</span>
+        <span class="time">{{ currentTime }}</span>
+      </div>
+    </div>
+    <!-- 飞行数据覆盖层 -->
+    <div class="flight-data">
+      <div class="data-row">
+        <div class="data-item">D {{ horizontalDistance }}m</div>
+        <div class="data-item">H {{ altitude }}m</div>
+      </div>
+      <div class="data-row">
+        <div class="data-item">H.S {{ horizontalSpeed }}m/s</div>
+        <div class="data-item">V.S {{ verticalSpeed }}m/s</div>
+      </div>
+    </div>
+    <div class="video-box">
+      <video
+        id="videoElement"
+        controls
+        :width="height + 'px'"
+        :height="width + 'px'"
+        ref="videoRef"
+        x5-video-player-type="x5"
+        muted
+      ></video>
+    </div>
+  </div>
+</template>
+
+<script>
+import flvjs from "flv.js";
+import { getDroneFlightTask, flightTakeOff } from "@/api/home";
+export default {
+  name: "App",
+  data() {
+    return {
+      liveUrl: "",
+      width: 0,
+      height: 0,
+      currentTime: "",
+      altitude: 100,
+      horizontalDistance: 0,
+      horizontalSpeed: 0,
+      verticalSpeed: 0,
+      timer: null,
+      dronePosition: {
+        lat: 39.9042,
+        lng: 116.4074,
+      },
+      flvPlayer: null,
+      flightTaskUuid: null,
+    };
+  },
+  computed: {
+    style() {
+      return {
+        width: this.width + "px",
+        height: this.height + "px",
+      };
+    },
+    orderInfo() {
+      return this.$store.state.orderInfo || {};
+    },
+  },
+  created() {
+    console.log("this.$router==>", this.$route);
+    this.width = this.$store.state.width;
+    this.height = this.$store.state.height;
+
+    this.$store.commit("SET_APP_VERSION", {
+      type: "currentPage",
+      value: 2,
+    });
+
+    this.$store.commit("SET_APP_VERSION", {
+      type: "router",
+      value: this.$router,
+    });
+
+    if (this.$route.query.flightTaskUuid) {
+      this.getLiveStreamUrl();
+      // 更新时间
+      this.updateTime();
+    } else {
+      // this.$message.error("参数错误");
+      // setTimeout(() => {
+      //   this.$router.back();
+      // }, 1000);
+    }
+  },
+  mounted() {
+    // 启动定时器更新时间和模拟飞行数据
+    this.startTimeUpdate();
+    // this.simulateFlightData();
+    // this.$router.removeRoute('OrderDetails')
+  },
+  // watch: {
+  //   $route(to, from) {
+  //     console.log("from==>", from);
+  //     console.log("to==>", to);
+  //     console.log("this.$route==>", this.$route);
+  //     if (from) {
+  //       this.$nextTick(() => {
+  //         this.$router.removeRoute(from.name);
+  //       });
+  //     }
+  //   },
+  // },
+  methods: {
+    initPlayer() {
+      console.log("initPlayer==>", 222222222);
+      console.log("flvjs.isSupported()==>", flvjs.isSupported());
+      if (flvjs.isSupported()) {
+        const videoElement = document.getElementById("videoElement");
+        const flvPlayer = flvjs.createPlayer({
+          type: "flv",
+          url: this.liveUrl, // 替换为你的直播流URL
+          enableStashBuffer: true, // 启用缓冲优化
+          stashInitialSize: 128, // 初始缓冲大小(单位KB)
+          isLive: true, // 表示这是一个直播流
+        });
+        this.flvPlayer = flvPlayer;
+        // setTimeout(() => {
+        //   console.log("videoRef==>", this.$refs.videoRef);
+        // }, 1000);
+        if (flvPlayer) {
+          flvPlayer.on(flvjs.Events.ERROR,  (eventType, detail)=> {
+            console.log("Error type:", eventType);
+            console.log("Error detail:", detail);
+            if(eventType == 'NetworkError') this.flvPlayer.pause();
+          });
+          flvPlayer.on(flvjs.Events.LOADING_COMPLETE,  (eventType, detail)=> {
+            console.log("MEDIA_INFO111111:", eventType);
+            console.log("MEDIA_INFO detail:", detail);
+            // if(eventType == 'NetworkError') this.flvPlayer.pause();
+            flvPlayer.play(); // 开始播放视频流
+            
+          });
+          try {
+            flvPlayer.attachMediaElement(videoElement);
+            flvPlayer.load(); // 加载视频流
+          } catch {}
+        }
+      } else {
+        // alert("当前浏览器不支持FLV.js");
+      }
+    },
+    getLiveStreamUrl() {
+      const loading = this.$loading({
+        lock: true,
+        text: "准备中请稍等",
+        spinner: "el-icon-loading",
+        background: "rgba(0, 0, 0, 0.7)",
+      });
+      // 实际项目中应该从API获取直播流地址
+      // 这里使用模拟数据
+      getDroneFlightTask({
+        flightTaskUuid: this.$route.query.flightTaskUuid,
+      })
+        .then((res) => {
+          loading.close();
+          if (res.code == 200 && res.data) {
+            this.info = res.data;
+            if (
+              res.data.startStreamConvert &&
+              res.data.startStreamConvert.videoUrl
+            ) {
+              this.liveUrl = res.data.startStreamConvert.videoUrl + ".flv";
+              try {
+                this.initPlayer();
+              } catch {}
+            }
+          }
+        })
+        .catch((err) => {
+          loading.close();
+          this.back();
+        });
+    },
+    getQueryParam(paramName) {
+      const urlParams = new URLSearchParams(window.location.search);
+      return urlParams.get(paramName);
+    },
+    updateTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, "0");
+      const day = String(now.getDate()).padStart(2, "0");
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      const seconds = String(now.getSeconds()).padStart(2, "0");
+      this.currentTime = `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
+    },
+    startTimeUpdate() {
+      this.timer = setInterval(() => {
+        this.updateTime();
+      }, 1000);
+    },
+    simulateFlightData() {
+      // 模拟飞行数据变化
+      setInterval(() => {
+        this.altitude = Math.floor(Math.random() * 10) + 95;
+        this.horizontalDistance = Math.floor(Math.random() * 5);
+        this.horizontalSpeed = (Math.random() * 0.5).toFixed(1);
+        this.verticalSpeed = (Math.random() * 0.5).toFixed(1);
+      }, 2000);
+    },
+    back() {
+      this.$router.replace(
+        `/?orderNumber=${this.$store.state.orderInfo.orderNumber}`
+      );
+    },
+  },
+  beforeDestroy() {
+    // 清除定时器
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+    if (this.flvPlayer) {
+      console.log("销毁播放器===》》》");
+      this.flvPlayer.pause();
+      this.flvPlayer.destroy(); // 销毁播放器实例,清理资源
+      this.flvPlayer = null;
+    }
+  },
+};
+</script>
+
+<style scoped>
+.video-container {
+  box-sizing: border-box;
+  overflow: hidden;
+  position: relative;
+  margin: 0 !important;
+  padding: 0;
+}
+.container-title {
+  position: absolute;
+  top: calc(50% - 20px);
+  left: -30px;
+  /* width: 100%; */
+  box-sizing: border-box;
+  background-color: rgba(0, 0, 0, 0.5);
+  padding: 10px 20px 10px 5px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  color: #ffffff;
+  font-size: 14px;
+  z-index: 20000;
+  transform: rotate(90deg);
+}
+.container-title .title-right {
+  flex-shrink: 0;
+  margin-left: 10px;
+  display: flex;
+  align-items: center;
+}
+.container-title .title-left {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+}
+
+.container-title .title-left img {
+  flex-shrink: 0;
+  width: 16px;
+  height: 16px;
+  margin-right: 5px;
+}
+
+.container-title .title-left .title-text {
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  box-sizing: border-box;
+}
+
+.container-title .title-right img {
+  width: 20px;
+  height: 20px;
+  margin-right: 5px;
+}
+
+.container-title .title-right .time {
+  margin-left: 10px;
+}
+
+.flight-data {
+  position: absolute;
+  bottom: 50%;
+  left: -25%;
+  width: 100%;
+  padding: 0;
+  box-sizing: border-box;
+  z-index: 20000;
+
+  transform: rotate(90deg);
+  color: #ffffff;
+}
+.flight-data .data-row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.flight-data .data-item {
+  min-width: 35%;
+  text-align: center;
+  margin-bottom: 10px;
+}
+.video-box {
+  transform: rotate(90deg);
+  box-sizing: border-box;
+  margin: 0;
+}
+#videoElement {
+  display: block;
+  object-fit: fill; /* 使用cover或fill */
+  margin: 0 !important;
+  z-index: 900;
+  position: relative;
+  /* position: absolute;
+  left: 0;
+  bottom: 0; */
+}
+</style>

+ 817 - 0
src/views/OrderDetails.vue

@@ -0,0 +1,817 @@
+<template>
+  <div class="container">
+    <!-- 顶部导航栏 -->
+    <!-- <div class="navbar">
+      <div class="navbar-left" @click="handleBack()">
+        <img src="@/assets/back1.png" alt="" />
+      </div>
+      <div class="navbar-title">详情</div>
+    </div> -->
+    <div class="content-box">
+      <!-- 待使用状态 -->
+      <div class="topBox">
+        <div class="topBox_lab" v-if="form.orderStatus == 10">待使用</div>
+        <div
+          class="topBox_lab"
+          v-if="form.orderStatus == 20 && !form.refundStatus"
+        >
+          已完成
+        </div>
+      </div>
+
+      <!-- 订单内容 -->
+      <div class="topBox" style="padding: 0px">
+        <div class="item">
+          <!-- 商家信息 -->
+          <div class="item_t">
+            <img v-if="form.businessImage" :src="form.businessImage" />
+            <div>{{ form.businessName }}</div>
+          </div>
+
+          <!-- 商品信息 -->
+          <div class="item_c">
+            <div class="item_c_l">
+              <img
+                v-if="form.businessProductImage || form.businessImage"
+                :src="form.businessProductImage || form.businessImage"
+              />
+            </div>
+            <div class="item_c_r">
+              <div class="item_c_r_title">{{ form.businessProductName }}</div>
+              <div class="item_c_r_text" v-if="form.productDataJosn">
+                {{ form.productDataJosn.costIncluded }}
+              </div>
+              <div class="item_c_r_price">
+                <div class="item_c_r_price_r">X{{ form.buyQuantity }}</div>
+                <div class="item_c_r_price_l" v-if="form.sellingPrice">
+                  <span>{{ form.sellingPrice.split(".")[0] }}</span
+                  >.{{ form.sellingPrice.split(".")[1] }}
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 合计金额 -->
+          <div class="item_c1">
+            <div class="item_c1_lab">合计:</div>
+            <div class="item_c1_val" v-if="form.orderAmount">
+              ¥ <span>{{ String(form.orderAmount).split(".")[0] }}</span
+              >.{{ String(form.orderAmount).split(".")[1] }}
+            </div>
+          </div>
+
+          <!-- 订单基本信息 -->
+          <div class="item_c2">
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">日期:</div>
+              <div class="item_c2_item_val">
+                {{
+                  form.orderTime
+                    ? form.orderTime.substr(0, 10).replace(/-/g, ".")
+                    : "--"
+                }}
+              </div>
+            </div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">购买数量:</div>
+              <div class="item_c2_item_val">x{{ form.buyQuantity }}</div>
+            </div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">游客信息:</div>
+              <div class="item_c2_item_val" v-if="form.refundOrderId">
+                {{ form.productOrder.productVisitor.visitorName }},{{
+                  form.productOrder.productVisitor.visitorPhone
+                }}
+              </div>
+            </div>
+          </div>
+
+          <!-- 订单详细信息 -->
+          <div class="item_c2">
+            <div class="item_c2_title">订单信息</div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">订单编号:</div>
+              <div class="item_c2_item_val">{{ form.orderNumber }}</div>
+            </div>
+            <div class="item_c2_item">
+              <div class="item_c2_item_lab">订单创建时间:</div>
+              <div class="item_c2_item_val">
+                {{ form.orderTime ? form.orderTime.replace(/-/g, ".") : "--" }}
+              </div>
+            </div>
+            <div
+              class="item_c2_item"
+              v-if="
+                form.orderType == 0 &&
+                (form.orderStatus == 10 ||
+                  form.orderStatus == 20 ||
+                  form.refundStatus == 2 ||
+                  form.refundStatus == 1)
+              "
+            >
+              <div class="item_c2_item_lab">支付方式:</div>
+              <div class="item_c2_item_val">
+                <div v-if="form.payMethod == 'wechat'">微信支付</div>
+                <div v-if="form.payMethod == 'balance'">余额支付</div>
+              </div>
+            </div>
+            <div
+              class="item_c2_item"
+              v-if="
+                form.orderType == 0 &&
+                (form.orderStatus == 10 ||
+                  form.orderStatus == 20 ||
+                  form.refundStatus == 2 ||
+                  form.refundStatus == 1)
+              "
+            >
+              <div class="item_c2_item_lab">支付时间:</div>
+              <div class="item_c2_item_val" v-if="form.payTime">
+                {{ form.payTime ? form.payTime.replace(/-/g, ".") : "--" }}
+              </div>
+              <div class="item_c2_item_val" v-else-if="form.productOrder">
+                {{
+                  form.productOrder.payTime
+                    ? form.productOrder.payTime.replace(/-/g, ".")
+                    : "--"
+                }}
+              </div>
+              <div class="item_c2_item_val" v-else>
+                {{
+                  form.productDataJosn.payTime
+                    ? form.productDataJosn.payTime.replace(/-/g, ".")
+                    : "--"
+                }}
+              </div>
+            </div>
+          </div>
+
+          <div class="item_c2" v-if="takeOffObj.queueStatus">
+            <div class="item_c2_title">排队信息</div>
+            <template v-if="!takeOffObj.flightStatus">
+              <div class="item_c2_item">
+                <div class="item_c2_item_lab">排队人员:</div>
+                <div class="item_c2_item_val">
+                  {{ takeOffObj.currentMyNum }}
+                </div>
+              </div>
+              <div class="item_c2_item">
+                <div class="item_c2_item_lab">等候时间:</div>
+                <div class="item_c2_item_val">
+                  {{ takeOffObj.waitMyTime }}分钟
+                </div>
+              </div>
+              <div class="line_up_btn" @click="getFlightCancelt">
+                <i class="el-icon-remove-outline"></i>
+                <span class="text">取消排队</span>
+              </div>
+            </template>
+            <div class="item_c2_item_prompt" v-else>
+              <img
+                src="https://wrj-songlanyn.oss-cn-beijing.aliyuncs.com/static/img/icon_1.png"
+                mode=""
+              />
+              <div class="text">
+                请尽快操作起飞,3分钟内未起飞将取消资格,取消后需要重新操作排队
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部按钮 -->
+      <div
+        class="btnBox1"
+        v-if="
+          form.orderStatus == 10 &&
+          takeOffObj.flightStatus != null &&
+          (takeOffObj.flightStatus || !takeOffObj.queueStatus)
+        "
+      >
+        <!-- <div
+          class="btnBox_l"
+          @click="goPath('/pages/order/applicationDrawback')"
+        >
+          申请退款
+        </div> -->
+        <div
+          class="btnBox_r"
+          v-if="takeOffObj.flightStatus"
+          @click="setTakeOff()"
+        >
+          起飞拍摄
+        </div>
+        <div
+          class="btnBox_r"
+          v-if="!takeOffObj.flightStatus && !takeOffObj.queueStatus"
+          @click="lineUpRefClick()"
+        >
+          排队起飞
+        </div>
+      </div>
+    </div>
+    <LineUp ref="lineUpRef" @success="getFlightListAll()" />
+  </div>
+</template>
+
+<script>
+import wx from "weixin-js-sdk";
+import {
+  orderInfo,
+  flightTakeOff,
+  flightStart,
+  flightCancelt,
+  flightListAll,
+  getDroneFlightTask,
+} from "@/api/home.js";
+import LineUp from "@/components/LineUp.vue";
+// import { mapState } from "vuex";
+export default {
+  name: "OrderDetails",
+  components: { LineUp },
+  data() {
+    return {
+      form: {},
+      couponList: [],
+      disableScroll: false,
+      intervalId: null,
+      takeOffObj: {},
+    };
+  },
+  created() {
+    // 获取订单信息
+    const query = this.$route.query;
+    if (query.orderNumber) {
+      this.getOrderInfo(query.orderNumber);
+    }
+    this.$store.commit("SET_APP_VERSION", {
+      type: "currentPage",
+      value: 1,
+    });
+
+    // console.log(this.$router);
+    this.$store.commit("SET_APP_VERSION", {
+      type: "router",
+      value: this.$router,
+    });
+
+  },
+  computed: {
+    takeOffShow() {
+      return this.$store.state.takeOffShow;
+    },
+    taskNum() {
+      return this.$store.state.taskNum;
+    },
+  },
+  watch: {
+    taskNum(val) {
+      console.log("taskNum", val);
+      if (val) {
+        if (this.$store.state.currentPage == 1) this.setTakeOff();
+      }
+    },
+    takeOffShow(val) {
+      console.log("takeOffShow", val);
+      if (val) {
+        this.getFlightListAll();
+      }
+    },
+    // $route(to, from) {
+    //   // console.log("this.$route.query==>", this.$route.query);
+    //   // this.width = this.$store.state.width;
+    //   // this.height = this.$store.state.height;
+    //   console.log("from==>", from);
+    //   console.log("to==>", to);
+    //   console.log("this.$route==>", this.$route);
+    //   if (from) {
+    //     this.$nextTick(() => {
+    //       this.$router.removeRoute(from.name);
+    //     });
+    //   }
+    // },
+  },
+  methods: {
+    getOrderInfo(orderNumber) {
+      // 实际项目中应该调用API
+      orderInfo(orderNumber).then((res) => {
+        this.form = res.data;
+        this.form.productDataJosn = JSON.parse(this.form.productData);
+        this.$store.commit("SET_APP_VERSION", {
+          type: "orderInfo",
+          value: res.data,
+        });
+        if (res.data.orderStatus == 10) {
+          console.log(res.data.orderStatus);
+          this.$nextTick(() => {
+            this.getFlightListAll(true);
+          });
+        }
+      });
+    },
+    goPath(path) {
+      this.$router.push(path);
+    },
+    // 起飞
+    setTakeOff() {
+      if (this.takeOffObj.flightStatus) {
+        const loading = this.$loading({
+          lock: true,
+          text: "起飞准备中",
+          spinner: "el-icon-loading",
+          background: "rgba(0, 0, 0, 0.7)",
+        });
+        flightTakeOff({
+          orderId: this.form.orderId,
+          productId: this.form.businessProductId,
+        })
+          .then((ret) => {
+            console.log("ret.data===>", ret.data);
+            if (ret.code == 200) {
+              this.$store.commit("SET_APP_VERSION", {
+                type: "flightTaskUuid",
+                value: ret.data.flightTaskUuid,
+              });
+              this.$nextTick(() => {
+                if (ret.data.startStreamConvert) {
+                  loading.close();
+                  console.log('跳转==>', 1111);
+                  this.$router.push(
+                    `/droneOperation?t=${new Date().getTime()}&flightTaskUuid=${
+                      ret.data.flightTaskUuid
+                    }`
+                  );
+                  return;
+                }
+                this.$store.commit("SET_APP_VERSION", {
+                  type: "loading",
+                  value: loading,
+                });
+              });
+            }
+          })
+          .catch((err) => {
+            loading.close();
+            // console.log(err);
+            if (err.code == 500) {
+              this.$refs.lineUpRef.open({
+                orderId: this.form.orderId,
+                productId: this.form.businessProductId,
+              });
+            }
+          });
+
+        return;
+      } else {
+        this.lineUpRefClick();
+      }
+    },
+    // 排队弹框
+    lineUpRefClick() {
+      this.$refs.lineUpRef.open({
+        orderId: this.form.orderId,
+        productId: this.form.businessProductId,
+      });
+    },
+    getFlightListAll(show = false) {
+      // 实际项目中应该调用API
+      flightListAll({
+        orderId: this.form.orderId,
+        productId: this.form.businessProductId,
+      }).then((res) => {
+        if (res.code == 200) {
+          this.takeOffObj = res.data;
+          console.log(show);
+          if (show) {
+            // console.log("开始执行");
+            this.$nextTick(() => {
+              this.$store.dispatch("initWebSocketUtil");
+            });
+          }
+        }
+      });
+    },
+    // 取消排队
+    getFlightCancelt() {
+      this.$confirm("是否确认取消排队?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      })
+        .then(() => {
+          flightCancelt({
+            orderId: this.form.orderId,
+            productId: this.form.businessProductId,
+            queueId: this.takeOffObj.id,
+          }).then((res) => {
+            if (res.code == 200) {
+              this.$message.success("取消成功");
+              this.takeOffObj = {};
+            }
+          });
+        })
+        .catch(() => {});
+    },
+    handleBack() {
+      this.$store.dispatch("disconnect");
+      wx.miniProgram.navigateBack();
+    },
+  },
+};
+</script>
+
+<style scoped>
+.container {
+  /* padding-top: 44px; */
+  padding-bottom: 60px;
+  background-color: #f8f8f8;
+  min-height: 100vh;
+  position: relative;
+  box-sizing: border-box;
+}
+
+.content-box {
+  padding: 16px 16px;
+}
+
+.disableScroll {
+  overflow: hidden;
+}
+
+/* 导航栏样式 */
+.navbar {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  height: 44px;
+  background-color: #c90700 !important;
+  position: fixed;
+  top: 0;
+  left: 0;
+  padding: 0 15px;
+  /* box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); */
+}
+
+.navbar-left {
+  width: 18px;
+  height: 18px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+}
+
+.navbar-left img {
+  width: 18px;
+  height: 18px;
+}
+
+.navbar-title {
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+  font-size: 16px;
+  font-weight: 500;
+  color: #ffffff;
+}
+
+.topBox {
+  background-color: #fff;
+  color: #333;
+  padding: 15px;
+  /* text-align: center; */
+  margin-bottom: 10px;
+  border-radius: 15px;
+}
+
+.topBox:first-of-type {
+  background-color: #fff;
+  border-bottom: 1px solid #eee;
+}
+
+.topBox_lab {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  text-align: center;
+}
+
+.topBox_val {
+  font-size: 20px;
+}
+
+.topBox_val span {
+  font-size: 32px;
+  font-weight: bold;
+}
+
+.topBox_time {
+  font-size: 14px;
+}
+
+.topBox_time span {
+  color: #ff4d4f;
+}
+
+.item {
+  background-color: #fff;
+  margin: 0;
+  padding: 15px;
+  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
+  border-radius: 15px;
+  box-sizing: border-box;
+}
+
+.item_t {
+  display: flex;
+  align-items: center;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+}
+
+.item_t img {
+  width: 40px;
+  height: 40px;
+  border-radius: 10px;
+  margin-right: 10px;
+  object-fit: cover;
+}
+
+.item_c {
+  display: flex;
+  padding: 15px 0;
+  border-bottom: 1px solid #eee;
+}
+
+.item_c_l img {
+  width: 94px;
+  height: 94px;
+  border-radius: 4px;
+  object-fit: cover;
+  flex-shrink: 0;
+}
+
+.item_c_r {
+  flex: 1;
+  margin-left: 10px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  text-align: left;
+}
+
+.item_c_r_title {
+  font-size: 12px;
+  /* font-weight: bold; */
+  margin-bottom: 5px;
+  /* text-align: right; */
+}
+
+.item_c_r_text {
+  font-size: 12px;
+  color: #1a1a1a;
+  margin: 8px 0 11px;
+  overflow: hidden;
+  min-height: 28px;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.item_c_r_price {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  /* margin-top: 10px; */
+}
+
+.item_c_r_price_l {
+  color: #ff4d4f;
+  font-size: 16px;
+}
+
+.item_c_r_price_l span {
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.item_c_r_price_r {
+  color: #666;
+  font-size: 14px;
+}
+
+.font22 {
+  font-size: 14px;
+  color: #3c7eff;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+}
+
+.font22 i {
+  font-size: 12px;
+  margin-left: 3px;
+}
+
+.item_c1 {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 15px 0;
+  border-bottom: 1px solid #eee;
+}
+
+.item_c1_lab {
+  font-size: 14px;
+  color: #666;
+}
+
+.item_c1_val {
+  color: #ff4d4f;
+  font-size: 16px;
+}
+
+.item_c1_val span {
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.item_c2 {
+  padding: 15px 0;
+  border-bottom: 1px solid #eee;
+}
+
+.item_c2:last-child {
+  border-bottom: none;
+}
+
+.item_c2_title {
+  font-size: 15px;
+  font-weight: bold;
+  margin-bottom: 10px;
+  position: relative;
+  padding: 0 10px;
+  font-weight: 700;
+}
+
+.item_c2_title::before {
+  content: "";
+  /* 必须有content来触发::before */
+  position: absolute;
+  /* 绝对定位 */
+  top: 0;
+  /* 在顶部 */
+  left: 0;
+  /* 在左边 */
+  width: 4px;
+  height: 14px;
+  background-color: #fb0b03;
+  /* 横线的颜色 */
+  border-radius: 2px;
+}
+
+.item_c2_item {
+  display: flex;
+  margin-bottom: 10px;
+}
+
+.item_c2_item:last-child {
+  margin-bottom: 0;
+}
+
+.item_c2_item_lab {
+  width: 100px;
+  color: #666;
+  font-size: 14px;
+}
+
+.item_c2_item_val {
+  flex: 1;
+  font-size: 14px;
+  color: #333;
+  text-align: right;
+}
+
+.btnBox {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  background-color: #fff;
+  padding: 10px 15px;
+  /* box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); */
+}
+
+.btnBox_l {
+  flex: 1;
+  display: flex;
+  align-items: center;
+}
+
+.btnBox_l_lab {
+  font-size: 14px;
+  color: #666;
+}
+
+.btnBox_l_val {
+  color: #ff4d4f;
+  font-size: 16px;
+}
+
+.btnBox_l_val span {
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.btnBox_r {
+  width: 110px;
+  height: 40px;
+  /* background-color: #3c7eff; */
+  color: #fff;
+  border-radius: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  cursor: pointer;
+}
+
+.btnBox1 {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  background-color: #fff;
+  padding: 10px 15px;
+  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.btnBox1 .btnBox_l {
+  width: 100px;
+  height: 40px;
+  background-color: #fff;
+  color: #666;
+  border: 1px solid #ddd;
+  border-radius: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  margin-right: 15px;
+  cursor: pointer;
+}
+
+.btnBox1 .btnBox_r {
+  width: 110px;
+  height: 40px;
+  background-color: #ff0000;
+  color: #fff;
+  border-radius: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 15px;
+  cursor: pointer;
+}
+
+.item_c2_item_prompt {
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  padding-top: 7px;
+}
+.item_c2_item_prompt image {
+  flex-shrink: 0;
+  width: 35px;
+  height: 33rpx;
+}
+
+.item_c2_item_prompt .text {
+  margin-left: 10px;
+  color: #666666;
+}
+
+.line_up_btn {
+  width: 70px;
+  margin: 13px auto;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-bottom: 3px;
+  border-bottom: 1px solid #fb0b03;
+}
+
+.line_up_btn .text {
+  font-size: 13px;
+  color: #fb0b03;
+}
+.line_up_btn .el-icon-remove-outline {
+  color: #fb0b03;
+}
+</style>

+ 50 - 0
vue.config.js

@@ -0,0 +1,50 @@
+const {
+  defineConfig
+} = require('@vue/cli-service')
+module.exports = defineConfig({
+  lintOnSave: false,  // 关闭警告提醒
+  transpileDependencies: true,
+  publicPath: '/h5',
+  chainWebpack: config => {
+    // 配置网页标题
+    config.plugin('html').tap((args) => {
+      args[0].title = '详情'
+      return args
+    })
+  },
+  devServer: {
+    host: '0.0.0.0',
+    port: '8080',
+    open: true,
+    proxy: {
+      // detail: https://cli.vuejs.org/config/#devserver-proxy
+      "/prod-api": {
+        target: `http://192.168.0.151:9002`,
+        // target: `http://192.168.0.78:9002`,
+        // target: `https://test.yuemeikang.com/prod-api`,  // 测试环境
+        // target: `https://app.yuemeikang.com/prod-api`,  // 正式环境
+        changeOrigin: true,
+        pathRewrite: {
+          ['^' + '/prod-api']: ''
+        }
+      },
+      "/api": {
+        target: `http://192.168.0.151:9001`,
+        // target: `http://192.168.0.78:9001`,
+        // target: `https://test.yuemeikang.com/prod-api`,  // 测试环境
+        // target: `https://app.yuemeikang.com/prod-api`,  // 正式环境
+        changeOrigin: true,
+        pathRewrite: {
+          ['^' + '/api']: '/api'
+        }
+      }
+    }
+  },
+  css: {
+    loaderOptions: {
+      sass: {
+        sassOptions: { outputStyle: "expanded" }
+      }
+    }
+  }
+})