Browse Source

Merge remote-tracking branch 'origin/master'

S0025136190 1 year ago
parent
commit
487247fb2d

+ 18 - 0
Web/src/api/custom/index.ts

@@ -136,6 +136,8 @@ export type DeleteDirApiParams = {
 export type FetchBingCodeApi = () => Promise<any>;
 export type FetchJobListApi = (params: FetchJobListApiParams) => Promise<any>;
 export type DeleteJobApi = (params: { job_id: string }) => Promise<any>;
+export type FetchAllDockDeviceListApi = () => Promise<any>;
+export type FetchAllWaylineListApi = () => Promise<any>;
 export type FetchWaylineListApi = (params: FetchWaylineListApiParams) => Promise<any>;
 export type FetchDeviceLogListApi = (sn: string, params: { domain_list: string }) => Promise<any>;
 export type FetchDeviceFeedbackRecordListApi = (sn: string, params: FetchDeviceFeedbackRecordListApiParams) => Promise<any>;
@@ -189,6 +191,20 @@ const deleteJobApi: DeleteJobApi = async (params) => {
     return res.data;
 };
 
+// 获取全部机场设备列表
+const fetchAllDockDeviceListApi: FetchAllDockDeviceListApi = async () => {
+    const url = `/manage/api/v1/devices/${getWorkspaceId()}/getDockDevices`
+    const res = await request.get(url);
+    return res.data;
+};
+
+// 获取全部航线列表
+const fetchAllWaylineListApi: FetchAllWaylineListApi = async () => {
+    const url = `/wayline/api/v1/workspaces/${getWorkspaceId()}/getWaylines`
+    const res = await request.get(url);
+    return res.data;
+};
+
 // 获取航线列表
 const fetchWaylineListApi: FetchWaylineListApi = async (params) => {
     const url = `/wayline/api/v1/workspaces/${getWorkspaceId()}/waylines`
@@ -369,6 +385,8 @@ export const apis = {
     fetchBingCode: fetchBingCodeApi,
     fetchJobList: fetchJobListApi,
     deleteJob: deleteJobApi,
+    fetchAllDockDeviceList: fetchAllDockDeviceListApi,
+    fetchAllWaylineList: fetchAllWaylineListApi,
     fetchWaylineList: fetchWaylineListApi,
     fetchDeviceLogList: fetchDeviceLogListApi,
     fetchDeviceFeedbackRecordList: fetchDeviceFeedbackRecordListApi,

+ 4 - 10
Web/src/pages/page-web/projects/task/taskList/components/CreateTaskModal.vue

@@ -1,6 +1,6 @@
 <template>
-    <a-modal width="100%" title="新建机场任务" :closable="false" :destroyOnClose="true" :maskClosable="false"
-        wrapClassName="createTask-modal" :visible="visible" :footer="null">
+    <a-modal width="100%" :title="null" :footer="null" :closable="false" :destroyOnClose="true" :maskClosable="false"
+        wrapClassName="createTask-modal" :visible="visible">
         <div class="content">
             <div class="content-panel">
                 <TaskPanel :onClickConfirm="onClickConfirm" :onClickCancel="onClickCancel" />
@@ -54,11 +54,10 @@ onMounted(() => {
 .content {
     width: 100%;
     height: 100%;
-    padding: 20px;
     display: flex;
 
     &-panel {
-        width: 300px;
+        width: auto;
         height: 100%;
         background-color: #232323;
     }
@@ -66,17 +65,12 @@ onMounted(() => {
     &-map {
         flex: 1;
         height: 100%;
-        margin-left: 20px;
     }
 }
 </style>
 
 <style lang="scss">
 .createTask-modal {
-    .ant-modal-header {
-        height: 60px;
-    }
-
     .ant-modal {
         max-width: 100%;
         top: 0;
@@ -91,7 +85,7 @@ onMounted(() => {
     }
 
     .ant-modal-body {
-        height: calc(100vh - 60px);
+        height: 100vh;
         padding: 0;
     }
 }

+ 508 - 164
Web/src/pages/page-web/projects/task/taskList/components/TaskPanel.vue

@@ -1,154 +1,232 @@
 <template>
   <div class="taskPanel">
-    <a-spin :spinning="state.loading">
-      <a-form ref="formRef" layout="vertical" :hideRequiredMark="true" :model="state.formModel">
-        <!-- 任务名称 -->
-        <a-form-item label='任务名称' name="name" :rules="[{ required: true, message: '任务名称不能为空', whitespace: true }]">
-          <a-input placeholder="请输入任务名称" :maxlength="20" v-model:value="state.formModel.name" />
-        </a-form-item>
-        <!-- 执行航线 -->
-        <a-form-item label="执行航线" name="file_id" :rules="[{ required: true, message: '执行航线不能为空' }]">
-          <a-button type="primary">
-            选择航线
-          </a-button>
-          <div class="wayline-panel" style="padding-top: 5px;">
-            <div class="title">
-              <a-tooltip :title="wayline.name">
-                <div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
-                  {{ wayline.name }}</div>
-              </a-tooltip>
-              <div class="ml10">
+    <div class="taskPanel-info">
+      <div class="taskPanel-info-title">
+        新建机场任务
+      </div>
+      <a-spin :spinning="state.loading">
+        <div class="taskPanel-info-content">
+          <a-form ref="formRef" layout="vertical" :hideRequiredMark="true" :model="state.formModel">
+            <!-- 任务名称 -->
+            <a-form-item label='任务名称' name="name" :rules="[{ required: true, message: '任务名称不能为空', whitespace: true }]">
+              <a-input placeholder="请输入任务名称" :maxlength="20" v-model:value="state.formModel.name" />
+            </a-form-item>
+            <!-- 执行航线 -->
+            <a-form-item label="执行航线" name="file_id" :rules="[{ required: true, message: '执行航线不能为空' }]">
+              <a-button type="primary" @click="handleClickSelectWayline">
+                <PlusOutlined />
+                选择航线
+              </a-button>
+              <div v-if="waylineInfo">
+                <div class="wayline-panel">
+                  <div class="wayline-panel-title">
+                    <div class="wayline-panel-title-left">
+                      {{ waylineInfo.name }}
+                    </div>
+                    <div class="wayline-panel-title-right">
+                      <UserOutlined />
+                      {{ waylineInfo.user_name }}
+                    </div>
+                  </div>
+                  <div style="margin-bottom: 5px;">
+                    <img :src="aircraftSrc">
+                    {{ waylineInfo.drone_model }}
+                  </div>
+                  <div style="margin-bottom: 5px;">
+                    <CameraFilled />
+                    {{ waylineInfo.payload_model }}
+                  </div>
+                  <div>
+                    更新时间 {{ waylineInfo.update_time }}
+                  </div>
+                </div>
+              </div>
+            </a-form-item>
+            <!-- 执行设备 -->
+            <a-form-item label="执行设备" name="dock_sn" :rules="[{ required: true, message: '执行设备不能为空' }]">
+              <a-button type="primary" @click="handleClickSelectDevice">
+                <PlusOutlined />
+                选择设备
+              </a-button>
+              <div v-if="deviceInfo">
+                <div class="device-panel">
+                  <div class="device-panel-cell">
+                    <img :src="dockSrc">
+                    <div>
+                      {{ deviceInfo.name }}
+                    </div>
+                  </div>
+                  <div class="device-panel-cell">
+                    <img :src="aircraftSrc">
+                    <div>
+                      {{ deviceInfo.children.name }}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </a-form-item>
+            <!-- 任务精度 -->
+            <a-form-item label="任务精度" name="wayline_precision_type">
+              <a-radio-group button-style="solid" v-model:value="state.formModel.wayline_precision_type">
+                <a-radio-button :value="1">
+                  高精度RTK
+                </a-radio-button>
+                <a-radio-button :value="0">
+                  GNSS
+                </a-radio-button>
+              </a-radio-group>
+            </a-form-item>
+            <!-- 任务策略 -->
+            <a-form-item label="任务策略" name="task_type">
+              <div style="white-space: nowrap;">
+                <a-radio-group button-style="solid" v-model:value="state.formModel.task_type">
+                  <a-radio-button :value="0">
+                    立即
+                  </a-radio-button>
+                  <a-radio-button :value="1">
+                    定时
+                  </a-radio-button>
+                  <a-radio-button :value="2">
+                    循环
+                  </a-radio-button>
+                </a-radio-group>
+              </div>
+            </a-form-item>
+            <template v-if="state.formModel.task_type === 1">
+              <!-- 执行时间 -->
+              <a-form-item label="执行时间" name="task_time" :rules="[{ required: true, message: '执行时间不能为空' }]">
+                <a-date-picker style="width: 100%;" :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
+                  valueFormat="YYYY-MM-DD HH:mm" placeholder="请选择执行时间" v-model:value="state.formModel.task_time" />
+              </a-form-item>
+            </template>
+            <template v-if="state.formModel.task_type === 2">
+              <!-- 执行日期 -->
+              <a-form-item label="执行日期" name="task_days" :rules="[{ required: true, message: '执行日期不能为空' }]">
+                <a-range-picker style="width: 100%;" valueFormat="YYYY-MM-DD"
+                  :disabledDate="(current: any) => current < moment().subtract(1, 'days')"
+                  :placeholder="['开始日期', '结束日期']" v-model:value="state.formModel.task_days" />
+              </a-form-item>
+              <!-- 执行时间 -->
+              <a-form-item label="执行时间" name="task_periods" :rules="[{ validator: checkTaskPeriods, required: true }]">
+                <div style="display: flex;align-items: center;">
+                  <a-time-picker placeholder="开始时间" format="HH:mm" valueFormat="YYYY-MM-DD HH:mm"
+                    v-model:value="state.formModel.task_periods[0]" />
+                  <div style="color: #FFFFFF;">-</div>
+                  <a-time-picker placeholder="结束时间" format="HH:mm" valueFormat="YYYY-MM-DD HH:mm"
+                    v-model:value="state.formModel.task_periods[1]" />
+                </div>
+              </a-form-item>
+              <!-- 任务开始执行的电量 -->
+              <a-form-item label="任务开始执行的电量" name="min_battery_capacity">
+                <a-input-number :min="50" :max="100" :formatter="(value: number) => `${value}%`"
+                  :parser="(value: string) => value.replace('%', '')"
+                  v-model:value="state.formModel.min_battery_capacity">
+                </a-input-number>
+              </a-form-item>
+            </template>
+            <!-- 返航高度 -->
+            <a-form-item label="返航高度(相对机场返航高度)" name="rth_altitude">
+              <a-input-number style="width: 100%;" :min="20" :max="1500" v-model:value="state.formModel.rth_altitude" />
+            </a-form-item>
+            <!-- 自动断点续飞 -->
+            <a-form-item label="自动断点续飞" name="breakpoint_continuation">
+              <a-switch checked-children="开" un-checked-children="关"
+                v-model:checked="state.formModel.breakpoint_continuation" />
+            </a-form-item>
+          </a-form>
+        </div>
+      </a-spin>
+      <div class="taskPanel-info-footer">
+        <a-button style="background: #3c3c3c;margin-right: 10px;" @click="onClickCancel">
+          取消
+        </a-button>
+        <a-button type="primary" @click="handleClickConfirm">
+          确定
+        </a-button>
+      </div>
+    </div>
+    <div class="taskPanel-modal" v-if="waylineState.visible">
+      <div class="taskPanel-modal-title">
+        <div>
+          航线列表
+        </div>
+        <a style="color: #FFFFFF;">
+          <CloseOutlined @click="waylineState.visible = false" />
+        </a>
+      </div>
+      <a-spin :spinning="waylineState.listLoading">
+        <div class="taskPanel-modal-content" v-if="waylineState.list.length">
+          <div class="wayline-panel" v-for="item in waylineState.list" :key="item.id"
+            @click="state.formModel.file_id = item.id">
+            <div class="wayline-panel-title">
+              <div class="wayline-panel-title-left">
+                {{ item.name }}
+              </div>
+              <div class="wayline-panel-title-right">
                 <UserOutlined />
+                {{ item.user_name }}
               </div>
-              <a-tooltip :title="wayline.user_name">
-                <div class="ml5 pr10"
-                  style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{
-                    wayline.user_name }}</div>
-              </a-tooltip>
             </div>
-            <div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
-              <span>
-                <RocketOutlined />
-              </span>
-              <span class="ml5">{{ DEVICE_NAME[wayline.drone_model_key] }}</span>
-              <span class="ml10">
-                <CameraFilled style="border-top: 1px solid; padding-top: -3px;" />
-              </span>
-              <span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
-                {{ DEVICE_NAME[payload] }}
-              </span>
+            <div style="margin-bottom: 5px;">
+              <img :src="aircraftSrc">
+              {{ item.drone_model }}
+            </div>
+            <div style="margin-bottom: 5px;">
+              <CameraFilled />
+              {{ item.payload_model }}
             </div>
-            <div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
-              <span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span>
+            <div>
+              更新时间 {{ item.update_time }}
             </div>
           </div>
-        </a-form-item>
-        <!-- 执行设备 -->
-        <a-form-item label="执行设备" name="dock_sn" :rules="[{ required: true, message: '执行设备不能为空' }]">
-          <a-button type="primary">
-            选择设备
-          </a-button>
-          <div class="panel" style="padding-top: 5px;">
-            <div class="title">
-              <a-tooltip :title="dock.nickname">
-                <div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
-                  {{ dock.nickname }}</div>
-              </a-tooltip>
+        </div>
+        <a-empty style="margin-top: 20%;" :image="noDataSrc" :image-style="{ height: '60px' }" v-else />
+      </a-spin>
+    </div>
+    <div class="taskPanel-modal" v-if="deviceState.visible">
+      <div class="taskPanel-modal-title">
+        <div>
+          设备列表
+        </div>
+        <a style="color: #FFFFFF;">
+          <CloseOutlined @click="deviceState.visible = false" />
+        </a>
+      </div>
+      <a-spin :spinning="deviceState.listLoading">
+        <div class="taskPanel-modal-content" v-if="deviceState.list.length">
+          <div class="device-panel" v-for="item in deviceState.list" :key="item.sn"
+            @click="state.formModel.dock_sn = item.sn">
+            <div class="device-panel-cell">
+              <img :src="dockSrc">
+              <div>
+                {{ item.name }}
+              </div>
             </div>
-            <div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
-              <span>
-                <RocketOutlined />
-              </span>
-              <!-- <span class="ml5">{{ dock.children?.nickname ?? 'No drone' }}</span> -->
+            <div class="device-panel-cell">
+              <img :src="aircraftSrc">
+              <div>
+                {{ item.children.name }}
+              </div>
             </div>
           </div>
-        </a-form-item>
-        <!-- 任务精度 -->
-        <a-form-item label="任务精度" name="wayline_precision_type">
-          <a-radio-group button-style="solid" v-model:value="state.formModel.wayline_precision_type">
-            <a-radio-button :value="1">
-              高精度RTK
-            </a-radio-button>
-            <a-radio-button :value="0">
-              GNSS
-            </a-radio-button>
-          </a-radio-group>
-        </a-form-item>
-        <!-- 任务策略 -->
-        <a-form-item label="任务策略" name="task_type">
-          <div style="white-space: nowrap;">
-            <a-radio-group button-style="solid" v-model:value="state.formModel.task_type">
-              <a-radio-button :value="0">
-                立即
-              </a-radio-button>
-              <a-radio-button :value="1">
-                定时
-              </a-radio-button>
-              <a-radio-button :value="2">
-                循环
-              </a-radio-button>
-            </a-radio-group>
-          </div>
-        </a-form-item>
-        <template v-if="state.formModel.task_type === 1">
-          <!-- 执行时间 -->
-          <a-form-item label="执行时间" name="select_execute_date_time" :rules="[{ required: true, message: '执行时间不能为空' }]">
-            <a-date-picker style="width: 100%;" show-time valueFormat="YYYY-MM-DD HH:mm:ss" placeholder="请选择执行时间"
-              v-model:value="state.formModel.select_execute_date_time" />
-          </a-form-item>
-        </template>
-        <template v-if="state.formModel.task_type === 2">
-          <!-- 执行日期 -->
-          <a-form-item label="执行日期" name="select_execute_range_date" :rules="[{ required: true, message: '执行日期不能为空' }]">
-            <a-range-picker style="width: 100%;" valueFormat="YYYY-MM-DD"
-              :disabledDate="(current: any) => current < moment().subtract(1, 'days')" :placeholder="['开始日期', '结束日期']"
-              v-model:value="state.formModel.select_execute_range_date" />
-          </a-form-item>
-          <!-- 执行时间 -->
-          <a-form-item label="执行时间" name="select_execute_range_time">
-            <a-range-picker style="width: 100%;" :show-time="{ format: 'HH:mm' }" format="HH:mm" valueFormat="HH:mm"
-              :placeholder="['开始时间', '结束时间']" v-model:value="state.formModel.select_execute_range_time" />
-          </a-form-item>
-          <!-- 任务开始执行的电量 -->
-          <a-form-item label="任务开始执行的电量" name="min_battery_capacity">
-            <a-input-number :min="50" :max="100" :formatter="(value: number) => `${value}%`"
-              :parser="(value: string) => value.replace('%', '')" v-model:value="state.formModel.min_battery_capacity">
-            </a-input-number>
-          </a-form-item>
-        </template>
-        <!-- 返航高度 -->
-        <a-form-item label="返航高度(相对机场返航高度)" name="rth_altitude">
-          <a-input-number style="width: 100%;" :min="20" :max="1500" v-model:value="state.formModel.rth_altitude" />
-        </a-form-item>
-        <!-- 自动断点续飞 -->
-        <a-form-item label="自动断点续飞" name="breakpoint_continuation">
-          <a-switch checked-children="开" un-checked-children="关"
-            v-model:checked="state.formModel.breakpoint_continuation" />
-        </a-form-item>
-        <!-- 自动断点续飞 -->
-        <a-form-item>
-          <div class="taskPanel-footer">
-            <a-button style="background: #3c3c3c;margin-right: 10px;" @click="onClickCancel">
-              取消
-            </a-button>
-            <a-button type="primary" @click="handleClickConfirm">
-              确定
-            </a-button>
-          </div>
-        </a-form-item>
-      </a-form>
-    </a-spin>
+        </div>
+        <a-empty style="margin-top: 20%;" :image="noDataSrc" :image-style="{ height: '60px' }" v-else />
+      </a-spin>
+    </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, reactive } from 'vue'
-import { RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
-import { useMyStore } from '/@/store'
-import { WaylineFile } from '/@/types/wayline'
-import { Device, DEVICE_NAME } from '/@/types/device'
-import moment from 'moment'
-import { createPlan } from '/@/api/wayline'
+import { ref, reactive, watch, computed, onMounted } from 'vue';
+import { message } from 'ant-design-vue';
+import { PlusOutlined, CloseOutlined, UserOutlined, CameraFilled } from '@ant-design/icons-vue';
+import noDataSrc from '/@/assets/icons/no-data.png';
+import aircraftSrc from '/@/components/airport/icons/aircraft.svg';
+import dockSrc from '/@/components/airport/icons/dockInfo.svg';
+import moment from 'moment';
+import { createPlan } from '/@/api/wayline';
+import { apis } from '/@/api/custom';
+import { DEVICE_NAME } from '/@/types/device'
 
 interface Props {
   onClickConfirm: (data: any) => Promise<any>,
@@ -161,16 +239,6 @@ const props = withDefaults(defineProps<Props>(), {
 
 const formRef = ref();
 
-const store = useMyStore()
-
-const dock = computed<Device>(() => {
-  return store.state.dockInfo
-})
-
-const wayline = computed<WaylineFile>(() => {
-  return store.state.waylineInfo
-})
-
 interface State {
   loading: boolean,
   formModel: {
@@ -179,9 +247,9 @@ interface State {
     dock_sn: string;
     wayline_precision_type: number;
     task_type: number;
-    select_execute_date_time?: string;
-    select_execute_range_date?: any[],
-    select_execute_range_time?: any[],
+    task_time: string;
+    task_days: any[],
+    task_periods: any[],
     min_battery_capacity: number,
     rth_altitude: number,
     breakpoint_continuation: boolean;
@@ -196,47 +264,323 @@ const state: State = reactive({
     dock_sn: '',// 执行设备
     wayline_precision_type: 1,// 任务精度
     task_type: 0,// 任务策略
-    select_execute_date_time: undefined,// 定时-执行时间
-    select_execute_range_date: undefined,// 循环-执行日期
-    select_execute_range_time: undefined,// 循环-执行时间
+    task_time: '',// 定时-执行时间
+    task_days: [],// 循环-执行日期
+    task_periods: [],// 循环-执行时间
     min_battery_capacity: 90,// 循环-任务开始执行的电量
     rth_altitude: 20,// 返航高度
     breakpoint_continuation: true,// 自动断点续飞
   }
 })
 
+const waylineState = reactive({
+  visible: false,
+  listLoading: false,
+  list: [] as {
+    id: string,
+    name: string,
+    update_time: string,
+    user_name: string,
+    drone_model: string,
+    payload_model: string,
+  }[],
+})
+
+const waylineInfo = computed(() => {
+  const fileId = state.formModel.file_id;
+  return waylineState.list.find(item => item.id === fileId);
+});
+
+const deviceState = reactive({
+  visible: false,
+  listLoading: false,
+  list: [] as {
+    sn: string,
+    name: string,
+    drone_model: string,
+    children: {
+      sn: string,
+      name: string,
+    },
+  }[],
+})
+
+const deviceInfo = computed(() => {
+  const dockSn = state.formModel.dock_sn;
+  return deviceState.list.find(item => item.sn === dockSn);
+});
+
+// 获取航线列表
+const fetchWaylineList = async () => {
+  waylineState.listLoading = true;
+  try {
+    const res = await apis.fetchAllWaylineList();
+    const list = res.data.map((item: any) => {
+      return {
+        id: item.id,
+        name: item.name,
+        update_time: moment(item.update_time).format('YYYY-MM-DD HH:mm:ss'),
+        user_name: item.user_name,
+        drone_model: DEVICE_NAME[item.drone_model_key],
+        payload_model: item.payload_model_keys.map((key: any) => DEVICE_NAME[key]).join('、'),
+      }
+    })
+    waylineState.list = list;
+  } catch (error) {
+    console.error(error);
+  } finally {
+    waylineState.listLoading = false;
+  }
+}
+
+// 获取设备列表
+const fetchDeviceList = async () => {
+  deviceState.listLoading = true;
+  try {
+    const res = await apis.fetchAllDockDeviceList();
+    // 在线设备列表
+    const olineList = res.data.filter((item: any) => item.status);
+    const list = olineList.map((item: any) => {
+      return {
+        sn: item.device_sn,
+        name: item.nickname,
+        drone_model: item.children?.device_name || '',
+        children: {
+          sn: item.children?.device_sn || '',
+          name: item.children?.nickname || '',
+        }
+      }
+    })
+    deviceState.list = list;
+  } catch (error) {
+    console.error(error);
+  } finally {
+    deviceState.listLoading = false;
+  }
+}
+
+onMounted(async () => {
+  await Promise.all([
+    fetchWaylineList(),
+    fetchDeviceList(),
+  ])
+})
+
+watch(() => waylineState.visible, async (visible) => {
+  if (visible) {// 更新航线列表
+    await fetchWaylineList();
+  }
+});
+
+watch(() => deviceState.visible, async (visible) => {
+  if (visible) {// 更新设备列表
+    await fetchDeviceList();
+  }
+});
+
+// 检查执行时间
+const checkTaskPeriods = (rule: any, values: string) => {
+  if (values?.length) {
+    const startTime = values[0];
+    const endTime = values[1];
+    if (!startTime) {
+      return Promise.reject('开始时间不能为空');
+    } else if (!endTime) {
+      return Promise.reject('结束时间不能为空');
+    } else if (startTime > endTime) {
+      return Promise.reject('结束时间不能早于开始时间');
+    } else {
+      return Promise.resolve();
+    }
+  } else {
+    return Promise.reject('执行时间不能为空');
+  }
+};
+
+// 点击选择航线
+const handleClickSelectWayline = () => {
+  waylineState.visible = true;
+  deviceState.visible = false;
+}
+
+// 点击选择设备
+const handleClickSelectDevice = () => {
+  deviceState.visible = true;
+  waylineState.visible = false;
+}
+
 // 点击确定
 const handleClickConfirm = async () => {
-  formRef.value?.validateFields().then(async (values: any) => {
+  formRef.value?.validateFields().then((values: any) => {
+    const waylineDroneModel = waylineInfo.value?.drone_model;
+    const droneModel = deviceInfo.value?.drone_model;
+    if (waylineDroneModel !== droneModel) {
+      return message.warning('请选择相同型号的设备');
+    }
     const data = { ...values };
-    // await createPlan()
+    if (values.task_time) {
+      data.task_time = moment(values.task_time).valueOf();
+    }
+    if (values.task_days?.length === 2) {
+      const startDate = moment(values.task_days[0]).valueOf();
+      const endDate = moment(values.task_days[1]).valueOf();
+      data.task_days = [startDate, endDate];
+    }
+    if (values.task_periods?.length === 2) {
+      const startDate = moment(values.task_periods[0]).valueOf();
+      const endDate = moment(values.task_periods[1]).valueOf();
+      data.task_periods = [startDate, endDate];
+    }
     console.log(data, 'data');
+    // await createPlan()
   }).catch((error: any) => {
     console.error(error);
   });
 }
 </script>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .taskPanel {
-  width: 100%;
   height: 100%;
   color: #FFFFFF;
-  overflow-y: auto;
+  position: relative;
+
+  &-info {
+    width: 300px;
+    height: 100%;
+    padding: 0 10px 10px;
+    border-right: 1px solid #4f4f4f;
+    background-color: #232323;
+
+    &-title {
+      width: 100%;
+      height: 40px;
+      padding-left: 10px;
+      border-bottom: 1px solid #4f4f4f;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    &-content {
+      width: 100%;
+      height: calc(100vh - 100px);
+      overflow: auto;
+    }
+
+    &-footer {
+      width: 100%;
+      height: 60px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      button {
+        width: 48%;
+        color: #FFFFFF;
+        border: none;
+      }
+    }
+  }
+
+  &-modal {
+    width: 280px;
+    height: 100%;
+    padding: 0 10px 10px;
+    background-color: #232323;
+    position: absolute;
+    left: 100%;
+    top: 0;
+    z-index: 2;
+
+    &-title {
+      width: 100%;
+      height: 40px;
+      border-bottom: 1px solid #4f4f4f;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    &-content {
+      width: 100%;
+      height: calc(100vh - 50px);
+      overflow: auto;
+    }
+  }
+}
 
-  &-footer {
+.wayline-panel {
+  width: 100%;
+  padding: 10px;
+  background-color: #3c3c3c;
+  border-radius: 4px;
+  color: #FFFFFF;
+  cursor: pointer;
+  margin-top: 10px;
+
+  &-title {
     display: flex;
-    padding: 10px 0;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 5px;
 
-    button {
-      width: 45%;
-      color: #fff;
-      border: 0;
+    &-left {
+      width: 120px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
     }
+
+    &-right {
+      width: 80px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+  }
+
+  img {
+    width: 14px;
+    height: 14px;
   }
+}
+
+.device-panel {
+  width: 100%;
+  padding: 10px;
+  background-color: #3c3c3c;
+  border-radius: 4px;
+  color: #FFFFFF;
+  cursor: pointer;
+  margin-top: 10px;
 
+  &-cell {
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+
+    img {
+      width: 16px;
+      height: 16px;
+      margin-right: 5px;
+    }
+  }
+
+  &-cell:last-child {
+    margin: 0;
+  }
+}
+</style>
+
+<style lang="scss">
+.taskPanel {
   form {
-    margin: 10px;
+    margin: 12px;
+  }
+
+  .ant-form-item {
+    margin-bottom: 12px;
   }
 
   form,

+ 16 - 9
Web/src/pages/page-web/projects/task/taskList/index.vue

@@ -11,14 +11,17 @@
             全选
           </a-checkbox>
         </div>
-        <div v-for="(dock, index) in state.onlineDockList" :key="dock.sn">
-          <div :class="[
-            'taskList-left-item',
-            checkState.checkSnList.includes(dock.sn) ? 'taskList-left-item-selected' : ''
-          ]" @click="onClickCheckItem(dock.sn)">
-            <Airport :dock="dock" :look-info="false" />
+        <div v-if="state.onlineDockList.length">
+          <div v-for="(dock, index) in state.onlineDockList" :key="dock.sn">
+            <div :class="[
+              'taskList-left-item',
+              checkState.checkSnList.includes(dock.sn) ? 'taskList-left-item-selected' : ''
+            ]" @click="onClickCheckItem(dock.sn)">
+              <Airport :dock="dock" :look-info="false" />
+            </div>
           </div>
         </div>
+        <a-empty style="margin-top: 20%;" :image="noDataSrc" :image-style="{ height: '60px' }" v-else />
         <div class="taskList-left-fill"></div>
       </div>
     </a-spin>
@@ -83,7 +86,10 @@
         <!-- 操作 -->
         <template #action="{ record }">
           <div class="flex-align-center flex-row" style="color: #2d8cf0">
-            <a-tooltip title="复制任务">
+            <a-tooltip title="断点续飞" v-if="record.breakpoint_continuation && [4, 5, 6].includes(record.status)">
+              <ApiOutlined style="margin-right: 10px;" />
+            </a-tooltip>
+            <a-tooltip title="复制任务" v-if="[0, 1].includes(record.task_type)">
               <CopyOutlined style="margin-right: 10px;" />
             </a-tooltip>
             <a-tooltip title="查看轨迹" v-if="false">
@@ -104,10 +110,11 @@
 <script lang="ts" setup>
 import { reactive, onMounted, watch, computed } from 'vue';
 import { Modal, message } from 'ant-design-vue';
-import { CopyOutlined, GatewayOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+import { ApiOutlined, CopyOutlined, GatewayOutlined, DeleteOutlined } from '@ant-design/icons-vue';
 import Search from './components/Search.vue';
 import Airport from '/@/components/airport/index.vue';
 import CreateTaskModal from './components/CreateTaskModal.vue';
+import noDataSrc from '/@/assets/icons/no-data.png';
 import { useMyStore } from '/@/store';
 import { useFormatTask } from '/@/components/task/use-format-task';
 import { apis } from '/@/api/custom';
@@ -435,6 +442,7 @@ const onClickDelete = (id: string, name: string) => {
     height: calc(100vh - 146px);
     padding: 10px 8px 0;
     background-color: #232323;
+    color: #FFFFFF;
     overflow-y: auto;
     margin-right: 20px;
 
@@ -442,7 +450,6 @@ const onClickDelete = (id: string, name: string) => {
       display: flex;
       justify-content: space-between;
       align-items: center;
-      color: #FFFFFF;
 
       .ant-checkbox-wrapper {
         color: #FFFFFF !important;

+ 3 - 2
Web/src/pages/page-web/projects/task/waylineList/index.vue

@@ -139,8 +139,9 @@ const columns = [
     title: '负载云台',
     dataIndex: 'payload_model_keys',
     width: 150,
-    customRender: ({ text }: any) => {
-      return DEVICE_NAME[text[0]]
+    customRender: ({ text }: { text: string[] }) => {
+      const payloadList = text.map(key => DEVICE_NAME[key]);
+      return payloadList.join('、');
     }
   },
   {