From 7d03329274a6a3054f3959be43423bebe08aeb64 Mon Sep 17 00:00:00 2001
From: chenyao <1219716595@qq.com>
Date: Fri, 21 Nov 2025 16:15:37 +0800
Subject: [PATCH] feat:智飞工单

---
 src/views/tickets/component/AddEditDetails.vue |  383 ++++++++++++++++++++++++++++++++++++++
 src/views/tickets/orderLog.vue                 |  169 ++++++++++++++--
 src/views/tickets/orderLog1.vue                |    2 
 src/views/tickets/component/SearchBox.vue      |    1 
 4 files changed, 530 insertions(+), 25 deletions(-)

diff --git a/src/views/tickets/component/AddEditDetails.vue b/src/views/tickets/component/AddEditDetails.vue
new file mode 100644
index 0000000..6cc025e
--- /dev/null
+++ b/src/views/tickets/component/AddEditDetails.vue
@@ -0,0 +1,383 @@
+<template>
+  <el-dialog
+    class="ztzf-dialog-mange"
+    modal-class="add-edit-details"
+    v-model="isShowAddEditDetails"
+    :title="txtTitle"
+    width="70%"
+    :close-on-click-modal="false"
+    :destroy-on-close="true"
+    @close="cancel"
+  >
+    <div class="add-edit-details">
+      <div class="process">
+        <div>{{ formParams.name }}}</div>
+        <div class="custom-steps-container">
+          <!-- 标题行 -->
+          <div class="steps-titles" v-if="filters.status !== '0'">
+            <div v-for="(record, index) in formParams.record_list" :class="{ active: record.user_id >= 0 }" :key="index"
+                 class="step-title">
+              {{ record.status_str }}
+            </div>
+          </div>
+
+          <!-- Element Steps 组件 -->
+          <el-steps :active="formParams.active" align-center class="custom-steps" v-if="filters.status !== '0'">
+            <el-step v-for="(record, index) in formParams.record_list" :key="index">
+              <template #description>
+                <span class="step-description" style="position: relative; display: inline-block">
+                  {{ record.user_name }}
+                </span>
+                <span class="step-timer">
+                  {{ record.interval_time_str }}
+                </span>
+                <div class="step-description">
+                  {{ record.create_time_str }}
+                </div>
+              </template>
+            </el-step>
+          </el-steps>
+        </div>
+      </div>
+      <el-form ref="orderFormRef" :model="formParams" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="工单名称" prop="name">
+              <el-input v-model="formParams.name" placeholder="请输入工单名称" maxlength="100" show-word-limit></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="关联航线" prop="file_id">
+              <el-select v-model="formParams.file_id" placeholder="请选择航线" filterable @change="getFlyingNest">
+                <el-option v-for="item in lineList" :key="item.wayline_id" :label="item.name"
+                           :value="item.wayline_id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="安全返航真高" prop="rth_altitude" class="safe-height">
+              <el-input-number v-model="formParams.rth_altitude" :min="50" :max="500"></el-input-number>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="关联机巢" prop="device_sns">
+              <el-select v-model="formParams.device_sns" placeholder="请选择机巢" multiple :disabled="!drone_sns.length">
+                <el-option v-for="item in drone_sns" :key="item.device_sn" :label="item.nickname" :value="item.device_sn" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="关联算法" prop="ai_types">
+              <el-select v-model="formParams.ai_types" placeholder="请选择关联算法" multiple>
+                <el-option v-for="item in aiTypeList" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="工单内容" prop="content">
+              <el-input type="textarea" v-model="formParams.content" rows="4" placeholder="请输入工单内容" maxlength="255" show-word-limit></el-input>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="周期频次" prop="date_range">
+              <el-date-picker
+                v-model="formParams.date_range"
+                type="daterange"
+                range-separator="至"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :disabled-date="disabledDate" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <div class="week_pc">
+              <div class="week_pc_one week-r">
+                <el-select v-model="formParams.rep_fre_type" placeholder="请选择频次">
+                  <el-option v-for="item in weeks" :key="item" :label="item" :value="item" />
+                </el-select>
+              </div>
+              <div class="week_pc_one">
+                <el-time-picker style="width: 100px" v-model="formParams.deal_time" prop="deal_time" value-format="HH:mm"
+                                :picker-options="{
+                    selectableRange: '00:00 - 23:59',
+                  }" />
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20" style="height: 400px">
+          <el-col :span="24">
+            <map-container v-if="isShowAddEditDetails" ref="mapContainerRef"></map-container>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+    <template #footer v-if="txtTitle === '新建工单'">
+      <div class="dialog-footer">
+        <el-button type="danger" @click="submitForm(1)" icon="el-icon-position">发起</el-button>
+        <el-button type="primary" @click="submitForm(0)" icon="el-icon-document-add">存草稿</el-button>
+        <el-button @click="cancel" icon="el-icon-circle-close">取消</el-button>
+      </div>
+    </template>
+    <template #footer v-if="txtTitle === '工单审核'">
+      <div class="dialog-footer">
+        <el-button type="primary"
+                   v-if="formParams.status == 1 && permission.order_log_review"
+                   @click="orderPass(formParams.id)">通过
+        </el-button>
+        <el-button type="danger"
+                   v-if="formParams.status == 1 && permission.rejection_btn"
+                   @click="orderReject(formParams.id)">驳回
+        </el-button>
+        <el-button @click="cancel" icon="el-icon-circle-close">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, shallowRef, onMounted, computed } from 'vue';
+import { newGetWorkspacesPage } from '@/api/resource/wayline'
+import { getDictionaryByCode } from '@/api/system/dictbiz'
+import { getFlyingNestBy } from '@/api/device/device'
+import {
+  saveUpdateOrderLog,
+  orderLogDetails,
+  orderLogPass,
+  orderLogReject,
+  orderLogRecall
+} from '@/api/tickets/orderLog';
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn' // 导入中文语言包
+import _ from 'lodash'
+import { ElMessage, ElMessageBox } from 'element-plus';
+import weekday from 'dayjs/plugin/weekday'
+dayjs.extend(weekday)
+dayjs.locale('zh-cn')
+import { useStore } from 'vuex'
+
+const emit = defineEmits(['refresh']);
+
+const store = useStore()
+const userInfo = computed(() => store.state.user.userInfo)
+const permission = computed(() => store.state.user.permission);
+
+const txtTitle = inject('txtTitle')
+const rowObj = inject('rowObj')
+
+const isShowAddEditDetails = defineModel('show')
+const mapContainerRef = ref(null)
+const orderFormRef = ref(null)
+const formParams = ref({})
+const rules = ref({
+  name: [
+    { required: true, message: '请输入工单名称', trigger: 'blur' },
+    { max: 100, message: '工单名称不能超过100个字', trigger: 'blur' }
+  ],
+    file_id: [{ required: true, message: '需要选择航线', trigger: 'change' }],
+    device_sns: [{ required: true, message: '请选择机巢', trigger: 'change' }],
+    content: [
+    { required: true, message: '请输入工单内容', trigger: 'blur' },
+    { max: 255, message: '工单内容不能超过255个字', trigger: 'blur' }
+  ],
+    date_range: [{ required: true, message: '请选择时间不能为空', trigger: 'blur' }],
+    rep_fre_type: [{ required: true, message: '请选择周期', trigger: 'blur' }],
+    deal_time: [{ required: true, message: '请选择执行时间', trigger: 'blur' }]
+})
+//周期
+const weeks = ref(['每天', '周一', '周二', '周三', '周四', '周五', '周六', '周末', '周天', '工作日'])
+
+// 获取航线
+let lineList = ref([])
+function getLines() {
+  const formData = {
+    name: '',
+    waylineType: undefined,
+    current: 1,
+    size: 99999999,
+    descs: 'update_time'
+  }
+  newGetWorkspacesPage(formData).then(res => {
+    lineList.value = res.data.data.records
+  })
+}
+
+// 关联机巢
+const drone_sns = ref([])
+function getFlyingNest (waylineId) {
+  // 选中航线对象
+  initMapLine({}, polygon => {
+    const currentLine = lineList.value.find(item => item.wayline_id === waylineId)
+    const params = {
+      type: ['2', '4', '5'].includes(currentLine.wayline_type) ? 2 : 0,
+      wayline_id: waylineId,
+      polygon
+    }
+    getFlyingNestBy(params).then(res => {
+      drone_sns.value = res.data.data
+      initMapLine()
+    })
+  })
+}
+
+// 获取算法
+let aiTypeList = ref([])
+function aitypeList() {
+  getDictionaryByCode('SF').then(res => {
+    aiTypeList.value = res.data.data['SF']
+  })
+}
+
+// initMapLine 方法
+const initMapLine = async (infos = {}, cb = () => {}) => {
+  const currentLine = lineList.value.find(item => item.wayline_id == formParams.value.file_id)
+
+  if (!currentLine) return
+
+  await nextTick()
+
+  if (mapContainerRef.value?.initAddEntity) {
+    mapContainerRef.value.initAddEntity('polyline', {
+      url: `${import.meta.env.VITE_APP_AIRLINE_URL + currentLine.object_key}?_t=${new Date().getTime()}`,
+      type: currentLine.wayline_type,
+      cb,
+      infos
+    })
+  }
+}
+
+function formatDate (date) {
+  if (!date) return undefined
+  const d = new Date(date)
+  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
+    d.getDate()
+  ).padStart(2, '0')} 00:00:00`
+}
+
+function submitForm(status) {
+  orderFormRef.value?.validate(async (valid, fields) => {
+    if (valid) {
+      console.log('表单验证通过')
+      formParams.value.begin_time = formatDate(formParams.value.date_range[0])
+      formParams.value.end_time = formatDate(formParams.value.date_range[1])
+      // 如果选中日期包含当前天,那么选中的时间点不能小于当前时间
+      if (formParams.value.deal_time) {
+        const selectedDate = dayjs(formParams.value.date_range[0]).format('YYYY-MM-DD')
+        const selectedTime = dayjs(selectedDate + ' ' + formParams.value.deal_time).toDate()
+        const now = new Date()
+
+        if (selectedDate === dayjs().format('YYYY-MM-DD') && selectedTime < now) {
+          return ElMessage({message: '任务时间不能小于当前时间', type: 'warning'})
+        }
+      }
+      const submitData = {
+        ...formParams.value,
+        status: status,
+        ai_types: formParams.value.ai_types?.length ? formParams.value.ai_types : []
+      }
+      await saveUpdateOrderLog(submitData)
+      if (txtTitle === '新建工单') {
+        ElMessage({message: '工单创建成功', type: 'success'})
+      } else {
+        ElMessage({message: '工单发布成功', type: 'success'})
+      }
+      cancel()
+    } else {
+      console.log('表单验证失败', fields)
+    }
+  })
+}
+// 审核通过
+function orderPass(id) {
+  orderLogPass(id).then(res => {
+    ElMessage.success('审核通过')
+    cancel()
+  })
+}
+// 驳回
+const orderReject = async (id) => {
+  try {
+    const { value } = await ElMessageBox.prompt('', '驳回原因', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      inputPlaceholder: '请输入驳回原因',
+      inputValidator: (value) => {
+        if (!value) {
+          return '驳回原因不能为空'
+        }
+      }
+    })
+    // 用户输入了内容并点击确定
+    await orderLogReject(id, value)
+    ElMessage.success('驳回成功')
+    cancel()
+  } catch (error) {
+    // 用户点击取消或关闭弹窗
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('驳回失败')
+      console.error('驳回错误:', error)
+    }
+  }
+}
+
+// 监听txtTitle
+watch(txtTitle, async (newVal, oldVal) => {
+  console.log('txtTitle 发生变化:', newVal, oldVal)
+  if (newVal === '工单审核') {
+    const response = await orderLogDetails(rowObj.value.id)
+    const data = response.data.data
+    formParams.value = {
+      ...data
+    }
+    drone_sns.value = data.device_list
+    initMapLine(data.device_map_infos)
+  }
+}, {
+  immediate: true // 立即执行一次
+})
+
+function cancel() {
+  isShowAddEditDetails.value = false
+  emit('refresh');
+}
+
+onMounted(async () => {
+  aitypeList()
+  getLines()
+});
+</script>
+
+<style lang="scss" scoped>
+.add-edit-details {
+  .safe-height {
+    :deep(.el-form-item__label) {
+      width: 120px !important;
+    }
+
+    :deep(.el-input-number) {
+      width: 180px !important;
+    }
+  }
+  .week_pc {
+    display: flex;
+  }
+
+  .week_pc_one {
+    flex: 1;
+    .week-r {
+      margin-right: 4px;
+    }
+
+    &> ::v-deep(div) {
+      width: 100% !important;
+    }
+  }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/tickets/SearchBox.vue b/src/views/tickets/component/SearchBox.vue
similarity index 99%
rename from src/views/tickets/SearchBox.vue
rename to src/views/tickets/component/SearchBox.vue
index b0a82fd..aec12e8 100644
--- a/src/views/tickets/SearchBox.vue
+++ b/src/views/tickets/component/SearchBox.vue
@@ -117,7 +117,6 @@
     descs: 'update_time'
   }
   newGetWorkspacesPage(formData).then(res => {
-    console.log(res, '5555');
     lineList.value = res.data.data.records
   })
 }
diff --git a/src/views/tickets/orderLog.vue b/src/views/tickets/orderLog.vue
index c73e4ef..d531fcc 100644
--- a/src/views/tickets/orderLog.vue
+++ b/src/views/tickets/orderLog.vue
@@ -4,7 +4,7 @@
     <el-tabs v-model="activeTab" @tab-click="handleTabChange">
       <el-tab-pane v-for="tab in tabsList" :key="tab.name" :label="`${tab.label} (${tab.count})`" :name="tab.name">
         <SearchBox @search="searchClick"></SearchBox>
-        <el-button v-if="hasAddBtnPermission() && activeTab != 'WAIT_AUDIT'" type="primary" icon="el-icon-plus" @click="handleAddOrder">新建工单
+        <el-button v-if="permission?.order_log_add && activeTab != 'WAIT_AUDIT'" type="primary" icon="el-icon-plus" @click="handleAddOrder">新建工单
         </el-button>
         <el-button type="success" plain icon="el-icon-download" @click="exportData">导出
         </el-button>
@@ -41,27 +41,27 @@
             <el-table-column prop="cycle_time_value" label="工单周期频次" align="center" show-overflow-tooltip />
             <el-table-column label="操作" width="200" align="center">
               <template #default="scope">
-                <template v-if="scope.row.status == 1">
-                  <el-button v-if="hasPaddingBtnPermission()" type="text" icon="el-icon-view" @click="handleCheckDetail(row)">审核</el-button>
+                <template v-if="scope.row.status === 1">
+                  <el-button type="text" icon="el-icon-view" v-if="permission.order_log_review" @click="handleCheckDetail(scope.row, '工单审核')">审核</el-button>
                   <!--待审核状态-->
-                  <el-button type="text" icon="el-icon-warning" v-if="hasRecallPaddingBtnPermission()" @click="orderLogRecall(scope.row.id)">撤回
+                  <el-button type="text" icon="el-icon-warning" v-if="permission.order_log_recall" @click="revokeOrder(scope.row.id)">撤回
                   </el-button>
                 </template>
                 <!--已驳回-->
-                <template v-if="scope.row.status == 2">
-                  <el-button type="text" icon="el-icon-warning" @click="rejectDetail(scope.row.id)">驳回原因</el-button>
-                  <el-button v-if="userInfo.user_id == row.create_user" type="text" icon="el-icon-view" @click="handleViewDetail(scope.row)">编辑</el-button>
-                  <el-button v-else type="text" icon="el-icon-view" @click="handleViewDetail(scope.row)">详情</el-button>
+                <template v-if="scope.row.status === 2">
+                  <el-button type="text" icon="el-icon-warning" @click="rejectReason(scope.row.id)">驳回原因</el-button>
+                  <el-button type="text" icon="el-icon-view" v-if="userInfo.user_id === scope.row.create_user" @click="handleViewDetail(scope.row, '工单编辑')">编辑</el-button>
+                  <el-button type="text" icon="el-icon-view" v-else @click="handleViewDetail(scope.row, '工单详情')">详情</el-button>
                 </template>
                 <!-- 已通过 -->
-                <template v-if="scope.row.status == 3">
-                  <el-button type="text" icon="el-icon-view" @click="handleViewDetail(scope.row)">详情</el-button>
+                <template v-if="scope.row.status === 3">
+                  <el-button type="text" icon="el-icon-view" @click="handleViewDetail(scope.row, '工单详情')">详情</el-button>
                 </template>
                 <!--草稿-->
-                <template v-if="scope.row.status == 0">
-                  <el-button type="text" icon="el-icon-edit" @click="handleViewDetail(scope.row)">编辑</el-button>
+                <template v-if="scope.row.status === 0">
+                  <el-button type="text" icon="el-icon-edit" @click="handleViewDetail(scope.row, '工单编辑')">编辑</el-button>
                   <el-button type="text" icon="el-icon-position" @click="userPublishPush(scope.row.id)">发布</el-button>
-                  <el-button type="text" icon="el-icon-delete" @click="deleteOrderLog(scope.row.id)">删除</el-button>
+                  <el-button type="text" icon="el-icon-delete" @click="deleteOrder(scope.row.id)">删除</el-button>
                 </template>
               </template>
             </el-table-column>
@@ -76,11 +76,15 @@
       </el-tab-pane>
     </el-tabs>
   </div>
+  <AddEditDetails v-model:show="isShowAddEditDetails" @refresh="refreshGetOrderList" />
 </template>
 
 <script setup>
-import SearchBox from './SearchBox.vue'
+import SearchBox from './component/SearchBox.vue'
+import AddEditDetails from './component/AddEditDetails.vue'
+import NProgress from 'nprogress'
 import { useStore } from 'vuex'
+import { downloadXls } from '@/utils/util'
 import {
   getList,
   saveUpdateOrderLog,
@@ -96,12 +100,16 @@
 } from '@/api/tickets/orderLog'
 import { cloneDeep } from 'lodash';
 import { computed, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus'
 
 const store = useStore()
 
+const userInfo = computed(() => store.state.user.userInfo)
 const permission = computed(() => store.state.user.permission);
 
-console.log(permission, '8888')
+console.log(userInfo,permission, '8888')
+
+const isShowAddEditDetails = ref(false)
 
 const activeTab = ref('all')
 // tab切换
@@ -132,7 +140,6 @@
     },
   }
   getList(apiParams).then(res => {
-    console.log(res.data.data.records,'99999999')
     orderListTable.value = res.data.data.records
     total.value = res.data.data.total
   })
@@ -158,20 +165,136 @@
   getTableList()
 }
 
-const refreshGetJobList = () => {
+const refreshGetOrderList = () => {
   orderListParams.current = 1
   orderListParams.size = 10
   getTableList()
 }
 
 // 新增按钮 和 导出按钮
-function hasAddBtnPermission () {
-  // undefined 或 false 都返回 false,只有 true 返回 true
-  console.log(permission?.value.order_log_add, '66666')
-  return permission && permission?.value.order_log_add
+// function hasAddBtnPermission () {
+//   return permission && permission?.value.order_log_add
+// }
+// 新建工单、编辑、详情
+const txtTitle = ref('新建工单')
+const rowObj = ref({})
+provide('txtTitle', txtTitle)
+provide('rowObj', rowObj)
+function handleAddOrder() {
+  isShowAddEditDetails.value = true
 }
-// 新建工单
-function handleAddOrder() {}
+// 详情、编辑
+function handleViewDetail(row, txt) {
+  txtTitle.value = txt
+  isShowAddEditDetails.value = true
+}
+
+// 审核
+function handleCheckDetail(row, txt) {
+  rowObj.value = row
+  txtTitle.value = txt
+  isShowAddEditDetails.value = true
+}
+
+// 导出
+async function exportData() {
+  try {
+    await ElMessageBox.confirm('是否导出智飞工单数据?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    })
+
+    NProgress.start()
+
+    const apiParams = {
+      ...cloneDeep(orderListParams),
+      searchParams: {
+        ...cloneDeep(orderListParams.searchParams)
+      },
+    }
+    const res = await orderLogExport(apiParams)
+
+    downloadXls(res.data, `智飞工单${dayjs().format('YYYY-MM-DD')}.xlsx`)
+    ElMessage.success('导出成功')
+
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('导出失败')
+      console.error('导出错误:', error)
+    }
+  } finally {
+    NProgress.done()
+  }
+}
+
+// 撤回工单
+async function revokeOrder(id) {
+  try {
+    await ElMessageBox.confirm('确定撤回到草稿箱?', '是否撤回', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    // 用户点击确定,执行撤回操作
+    await orderLogRecall(id)
+    // 显示成功消息
+    ElMessage.success('撤回成功')
+    // 触发搜索刷新
+    refreshGetOrderList()
+  } catch (error) {
+    // 用户点击取消,不执行任何操作
+    if (error !== 'cancel') {
+      ElMessage.error('撤回失败')
+      console.error('撤回错误:', error)
+    }
+  }
+}
+
+// 驳回原因
+async function rejectReason(id) {
+  try {
+    const res = await orderLogDetails(id)
+    const data = res.data.data
+
+    await ElMessageBox.confirm(data.remark, '驳回原因', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    // 用户点击确定后的逻辑 TODO
+    // form.value = { ...res.data.data }
+    // handleViewDetail(data)
+
+  } catch (error) {
+    // 用户点击取消,不执行任何操作
+    if (error !== 'cancel') {
+      ElMessage.error('操作失败')
+      console.error('错误:', error)
+    }
+  }
+}
+
+// 删除
+async function deleteOrder(id) {
+  try {
+    await ElMessageBox.confirm('确定删除吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    deleteOrderLog(id)
+    ElMessage.success('删除成功')
+    // 触发搜索刷新
+    refreshGetOrderList()
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('删除失败')
+      console.error('删除错误:', error)
+    }
+  }
+}
 
 onMounted(() => {
   getTableList()
diff --git a/src/views/tickets/orderLog1.vue b/src/views/tickets/orderLog1.vue
index 6954133..925b20b 100644
--- a/src/views/tickets/orderLog1.vue
+++ b/src/views/tickets/orderLog1.vue
@@ -56,7 +56,7 @@
 </template>
 
 <script setup>
-import SearchBox from './SearchBox.vue'
+import SearchBox from './component/SearchBox.vue'
 import {
   getList,
   saveUpdateOrderLog,

--
Gitblit v1.9.3