PanelQjchs.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <!-- 起降场划设面板 -->
  2. <template>
  3. <div class="panel-qjchs flex flex-col aside-left-inner">
  4. <div class="title-main">起降场规划</div>
  5. <el-steps :active="currentStep" finish-status="success" align-center>
  6. <el-step title="起降场划设" />
  7. <el-step title="起降场评估" />
  8. </el-steps>
  9. <div class="flex-1 relative mb-4">
  10. <Transition name="emerge-left">
  11. <el-form ref="formRef" v-show="currentStep === 0" :model="form" :rules="rules" label-position="left"
  12. size="large" class="p-form p-main" hide-required-asterisk>
  13. <el-form-item label="名称" prop="name">
  14. <el-input v-model="form.name" clearable></el-input>
  15. </el-form-item>
  16. <el-form-item label="无人机类型" prop="aircraftType">
  17. <el-select v-model="form.aircraftType" placeholder="">
  18. <el-option v-for="item in uavList" :label="item.label" :value="item.value" />
  19. </el-select>
  20. </el-form-item>
  21. <el-form-item label="时间类型" prop="type">
  22. <el-radio-group v-model="form.type" size="large">
  23. <el-radio-button label="长期" value="长期" />
  24. <el-radio-button label="临时" value="临时" />
  25. </el-radio-group>
  26. </el-form-item>
  27. <template v-if="form.type === '临时'">
  28. <el-form-item label="日期" prop="startDate">
  29. <el-date-picker v-model="form.startDate" value-format="YYYY-MM-DD" type="date"
  30. class="flex-1"></el-date-picker>
  31. <div class="mx-3">--</div>
  32. <el-date-picker v-model="form.endDate" value-format="YYYY-MM-DD" type="date"
  33. class="flex-1"></el-date-picker>
  34. </el-form-item>
  35. <el-form-item label="时间" prop="startTime">
  36. <el-time-select v-model="form.startTime" :max-time="form.endTime" placeholder="" start="00:00"
  37. step="00:15" end="23:45" class="flex-1" />
  38. <div class="mx-3">--</div>
  39. <el-time-select v-model="form.endTime" :min-time="form.startTime" placeholder="" start="00:00"
  40. step="00:15" end="23:45" class="flex-1" />
  41. </el-form-item>
  42. <el-form-item label="划设方式" prop="dataType">
  43. <el-radio-group v-model="form.dataType" size="large">
  44. <el-radio label="手动划设" value="手动划设" />
  45. <el-radio label="导入" value="导入" />
  46. </el-radio-group>
  47. </el-form-item>
  48. </template>
  49. <el-form-item label="类型" prop="drawType" v-if="form.dataType === '手动划设'">
  50. <el-radio-group v-model="form.drawType" size="large">
  51. <el-radio-button v-for="item in drawTypes" :label="item.label" :value="item.value" />
  52. </el-radio-group>
  53. </el-form-item>
  54. <el-form-item label="上传文件" prop="file" v-else>
  55. <el-upload v-model:file-list="form.file" action="" class="single-uplaod">
  56. <div class="upload-trigger">点击上传文件</div>
  57. </el-upload>
  58. </el-form-item>
  59. <el-form-item label="高度" prop="height" v-if="form.drawType !== 'funnel'">
  60. <el-input v-model="form.height" type="number" clearable>
  61. <template #suffix>
  62. <span>米</span>
  63. </template>
  64. </el-input>
  65. </el-form-item>
  66. <el-form-item label="顶半径" prop="topRadius" v-if="['mesh', 'point', 'funnel'].includes(form.drawType)">
  67. <el-input v-model="form.topRadius" type="number" clearable>
  68. <template #suffix>
  69. <span>米</span>
  70. </template>
  71. </el-input>
  72. </el-form-item>
  73. <el-form-item label="底半径" prop="bottomRadius" v-if="['mesh', 'funnel'].includes(form.drawType)">
  74. <el-input v-model="form.bottomRadius" type="number" clearable>
  75. <template #suffix>
  76. <span>米</span>
  77. </template>
  78. </el-input>
  79. </el-form-item>
  80. <template v-if="form.drawType === 'funnel'">
  81. <el-form-item label="顶高度" prop="topHeight">
  82. <el-input v-model="form.topHeight" type="number" clearable>
  83. <template #suffix>
  84. <span>米</span>
  85. </template>
  86. </el-input>
  87. </el-form-item>
  88. <el-form-item label="底高度" prop="bottomHeight">
  89. <el-input v-model="form.bottomHeight" type="number" clearable>
  90. <template #suffix>
  91. <span>米</span>
  92. </template>
  93. </el-input>
  94. </el-form-item>
  95. </template>
  96. <el-form-item label="绘制空域">
  97. <el-button class="btn-secondary" @click="handleDraw">绘制</el-button>
  98. </el-form-item>
  99. </el-form>
  100. </Transition>
  101. <Transition name="emerge-right">
  102. <div v-show="currentStep === 1" class="p-main flex flex-col pr-0">
  103. <el-form-item label="网格查询">
  104. <el-button class="btn-secondary ml-4" @click="handleInspect(true)">查询网格</el-button>
  105. <el-button class="btn-secondary ml-4" @click="handleInspect(false)">关闭网格</el-button>
  106. </el-form-item>
  107. <div class="flex-1 overflow-y-auto pr-2">
  108. <div v-if="isSafe" class="msg-safe mb-2">当前空域无冲突!</div>
  109. <template v-else>
  110. <div v-for="item in results" :key="item.title" class="mb-3">
  111. <div class="title-sub">
  112. {{ item.title }}
  113. <div class="flex-1 flex justify-between items-center pl-2">
  114. <span v-if="item.level" class="risk-label"
  115. :class="riskTypes.find(i => i.value === item.level).color">{{
  116. riskTypes.find(i => i.value === item.level).label
  117. }}</span>
  118. <i v-else></i>
  119. <span class="btn-inline" @click="handleReInspect">重新评估</span>
  120. </div>
  121. </div>
  122. <table class="table-default stripe mt-2">
  123. <thead>
  124. <tr>
  125. <th v-for="th in item.cols">{{ th }}</th>
  126. <th class="w-20">地图显示</th>
  127. </tr>
  128. </thead>
  129. <tbody>
  130. <tr v-for="row in item.details">
  131. <td v-for="key in Object.keys(item.cols)">{{ row[key] }}</td>
  132. <td class="w-20">
  133. <el-checkbox size="large" v-model="row.display"
  134. :disabled="(row.id !== '3-3' && !row.cubes.length) || (row.id === '3-3' && item.details.every(r => r.cubes.length === 0))"
  135. @change="val => toggleCubeDetail(row, val)"></el-checkbox>
  136. </td>
  137. </tr>
  138. </tbody>
  139. </table>
  140. </div>
  141. </template>
  142. </div>
  143. </div>
  144. </Transition>
  145. </div>
  146. <div class="text-center">
  147. <el-button v-if="currentStep === 0" :disabled="!hasDraw" class="btn-main" @click="toNext">下一步</el-button>
  148. <template v-if="currentStep === 1">
  149. <el-button class="btn-main" @click="toPrev">上一步</el-button>
  150. <el-button class="btn-main" @click="handleSave" :loading="loading.save">保存</el-button>
  151. </template>
  152. </div>
  153. </div>
  154. </template>
  155. <script setup>
  156. import { onBeforeUnmount, ref, watch, reactive, computed } from 'vue';
  157. import { drawArea, clearDraw, InspectCube, showShapes, InspectPathCube } from '@/utils/map/addLayer'
  158. import { useMapStore } from '@/store/map';
  159. import { uavList } from '@/utils/options';
  160. import { AddPlot } from '@/service/http';
  161. import { riskTypes } from '@/utils/options';
  162. import useLayoutStore from '@/store/layout';
  163. const layoutStore = useLayoutStore()
  164. const mapStore = useMapStore()
  165. const currentStep = ref(0)
  166. const form = ref({
  167. aircraftType: '01',
  168. dataType: '手动划设',
  169. type: '长期',
  170. drawType: 'funnel',
  171. height: '100',
  172. topHeight: '110',
  173. bottomHeight: '60',
  174. topRadius: '150',
  175. bottomRadius: '50'
  176. })
  177. const drawTypes = [
  178. { label: '漏斗', value: 'funnel', geoType: '5', shapeType: 'funnel' },
  179. { label: '圆台', value: 'mesh', geoType: '6', shapeType: 'frustum-cone' },
  180. { label: '圆锥', value: 'point', geoType: '4', shapeType: 'inverted-cone' }
  181. ]
  182. const rules = {
  183. name: [{ required: true, message: '请输入空域名称', trigger: 'none' }],
  184. height: [{ required: true, message: '此项不可为空', trigger: 'none' }],
  185. topRadius: [{ required: true, message: '此项不可为空', trigger: 'none' }],
  186. bottomRadius: [{ required: true, message: '此项不可为空', trigger: 'none' }],
  187. topHeight: [{ required: true, message: '此项不可为空', trigger: 'none' }],
  188. bottomHeight: [{ required: true, message: '此项不可为空', trigger: 'none' }],
  189. }
  190. const formRef = ref(null)
  191. const results = ref([
  192. {
  193. title: '空域评估',
  194. level: null,
  195. cols: { areaName: '冲突空域', time: '冲突时段', height: '冲突高度' },
  196. details: []
  197. },
  198. {
  199. title: '计划冲突',
  200. level: null,
  201. cols: { areaName: '冲突空域', time: '冲突时段', height: '冲突高度' },
  202. details: []
  203. },
  204. {
  205. title: '障碍物分析',
  206. level: null,
  207. cols: { object: '冲突空域', height: '冲突高度' },
  208. details: []
  209. },
  210. {
  211. title: '地面安全评估',
  212. level: null,
  213. cols: { element: '要素', coverage: '覆盖率' },
  214. details: []
  215. }
  216. ])
  217. const isSafe = computed(() => {
  218. return results.value.every(i => i.level === 1)
  219. })
  220. const hasDraw = ref(false)
  221. async function handleDraw() {
  222. formRef.value.validateField(['height', 'topRadius', 'bottomRadius', 'topHeight', 'bottomHeight'], async (isValid) => {
  223. if (!isValid) return
  224. if (hasDraw.value) {
  225. showShapes({
  226. id: 'qjchs_drew_non_editable',
  227. data: null
  228. })
  229. await new Promise((res) => {
  230. setTimeout(() => res(), 500);
  231. })
  232. }
  233. // 处理绘制
  234. drawArea({
  235. ...form.value,
  236. color: [0, 255, 0, 0.5]
  237. })
  238. })
  239. }
  240. function getShapeInfo() {
  241. let res = {}
  242. const { height, topRadius, bottomRadius, topHeight, bottomHeight } = form.value
  243. const geoType = drawTypes.find(i => i.value === form.value.drawType).geoType
  244. const { x, y, z } = drawGeometry
  245. switch (geoType) {
  246. case '4':
  247. res = {
  248. point: { x, y, z },
  249. height: +height,
  250. radius: +topRadius
  251. }
  252. break;
  253. case '5':
  254. res = {
  255. point: { x, y, z },
  256. coneHeight: +topHeight,
  257. cylinderHeight: +bottomHeight,
  258. topRadius: +topRadius,
  259. bottomRadius: +bottomRadius
  260. }
  261. break;
  262. case '6':
  263. res = {
  264. point: { x, y, z },
  265. height: +height,
  266. topRadius: +topRadius,
  267. bottomRadius: +bottomRadius
  268. }
  269. break;
  270. }
  271. return res
  272. }
  273. function handleInspect(show = true) {
  274. if (show) {
  275. layoutStore.toggleGlobalLoading(true)
  276. }
  277. InspectCube({
  278. type: drawTypes.find(i => i.value === form.value.drawType).shapeType,
  279. id: 'inspect_qjchs',
  280. show,
  281. shape: show ? getShapeInfo() : {}
  282. })
  283. }
  284. let drawGeometry
  285. watch(() => mapStore.draw_geometry, (val) => {
  286. console.log('draw_geometry:', val)
  287. drawGeometry = val
  288. hasDraw.value = true
  289. }, { deep: true })
  290. watch(() => mapStore.cubeResult, (val) => {
  291. console.log('cubeResult:', val)
  292. formatResult(val)
  293. }, { deep: true })
  294. function clearResults() {
  295. results.value.forEach(v => {
  296. v.details = []
  297. v.level = null
  298. })
  299. }
  300. function formatResult(res) {
  301. console.log('formatResult:', res)
  302. clearResults()
  303. clearAllCubeDetail()
  304. const { groundSafety, lineConflictArray, obstacle, spaceConflictArray, spaceRisk, lineRisk } = res
  305. results.value[0].level = spaceRisk
  306. results.value[1].level = lineRisk
  307. results.value[2].level = obstacle.risk
  308. results.value[3].level = groundSafety.risk
  309. results.value[0].details = spaceConflictArray.map((i, index) => ({
  310. id: `0-${index}`,
  311. areaName: i.gridName,
  312. height: i.heightRange,
  313. time: i.conflictTime,
  314. cubes: i.gridEntityList,
  315. display: false,
  316. }))
  317. results.value[1].details = lineConflictArray.map((i, index) => ({
  318. id: `1-${index}`,
  319. areaName: i.gridName,
  320. height: i.heightRange,
  321. time: i.conflictTime,
  322. cubes: i.gridEntityList,
  323. display: false
  324. }))
  325. results.value[2].details = [
  326. {
  327. id: '2-0', time: '01-249:00-01-25 10-00', object: '建筑物',
  328. height: obstacle.buildingConflictHeight, cubes: obstacle.buildingsConflictArray, display: false
  329. },
  330. {
  331. id: '2-1', time: '01-249:00-01-25 10-00', object: '建筑物缓冲区',
  332. height: obstacle.buildingBufferConflictHeight, cubes: obstacle.buildingsBufferConflictArray, display: false
  333. },
  334. ]
  335. const toPercent = (num) => {
  336. return Math.round(num * 100) + '%'
  337. }
  338. results.value[3].details = [
  339. { id: '3-0', element: '道路', coverage: toPercent(groundSafety.road), cubes: groundSafety.gridGoupMap.road, display: false },
  340. { id: '3-1', element: '河流', coverage: toPercent(groundSafety.river), cubes: groundSafety.gridGoupMap.river, display: false },
  341. { id: '3-2', element: '绿化', coverage: toPercent(groundSafety.green), cubes: groundSafety.gridGoupMap.green, display: false },
  342. { id: '3-3', element: '综合占比', coverage: toPercent(groundSafety.road + groundSafety.river + groundSafety.green), display: false },
  343. ]
  344. layoutStore.toggleGlobalLoading(false)
  345. }
  346. function handleDisplay(row, val) {
  347. console.log(row, val)
  348. }
  349. function toNext() {
  350. formRef.value.validate(valid => {
  351. if (valid) {
  352. layoutStore.toggleGlobalLoading(true)
  353. clearDraw()
  354. showShapes({
  355. id: 'qjchs_drew_non_editable',
  356. data: [{
  357. type: drawTypes.find(i => i.value === form.value.drawType).shapeType,
  358. shape: {
  359. ...getShapeInfo(),
  360. color: [0, 255, 0, 0.5]
  361. }
  362. }]
  363. })
  364. handleInspect(true)
  365. currentStep.value = 1
  366. }
  367. })
  368. }
  369. let addedCubeDetailIds = []
  370. function toggleCubeDetail(row, status) {
  371. console.log(row, status)
  372. if (row.id === '3-3') {
  373. // 处理综合占比
  374. results.value[3].details.forEach(gRow => {
  375. if (gRow.display !== status && gRow.cubes.length) {
  376. gRow.display = status
  377. toggleCubeDetail(gRow, status)
  378. }
  379. })
  380. } else {
  381. if (!row.cubes.length) return
  382. const id = 'hxhs_' + row.id
  383. if (status) {
  384. addedCubeDetailIds.push(id)
  385. } else {
  386. const targetIndex = addedCubeDetailIds.findIndex(i => i === id)
  387. addedCubeDetailIds.splice(targetIndex, 1)
  388. }
  389. InspectPathCube({
  390. id,
  391. show: status,
  392. points: row.cubes.map(c => ({
  393. x: c.x,
  394. y: c.y,
  395. z: c.z,
  396. color: c.color
  397. })),
  398. size: {
  399. x: row.cubes[0].boxSize.lonLength,
  400. y: row.cubes[0].boxSize.latLength,
  401. z: row.cubes[0].boxSize.height
  402. }
  403. })
  404. }
  405. }
  406. function clearAllCubeDetail() {
  407. addedCubeDetailIds.forEach(id => {
  408. InspectPathCube({
  409. id,
  410. show: false,
  411. })
  412. })
  413. addedCubeDetailIds = []
  414. }
  415. function handleReInspect() {
  416. layoutStore.toggleGlobalLoading(true)
  417. handleInspect(false)
  418. setTimeout(() => {
  419. handleInspect(true)
  420. }, 500);
  421. }
  422. function toPrev() {
  423. currentStep.value = 0
  424. handleInspect(false)
  425. clearAllCubeDetail()
  426. clearResults()
  427. }
  428. const loading = reactive({
  429. save: false
  430. })
  431. function handleSave() {
  432. loading.save = true
  433. let params = {
  434. ...form.value,
  435. spaceType: form.value.areaType,
  436. geoType: drawTypes.find(i => i.value === form.value.drawType).geoType,
  437. upRadius: form.value.topRadius,
  438. downRadius: form.value.bottomRadius,
  439. nature: form.value.type,
  440. drawType: '01',
  441. shape: JSON.stringify(getShapeInfo())
  442. }
  443. AddPlot(params).then(({ data: res }) => {
  444. if (res.msg === 'success') {
  445. ElMessage({ offset: 90, type: 'success', message: '保存成功' })
  446. } else {
  447. ElMessage({ offset: 90, type: 'error', message: `保存失败,${res.msg}` })
  448. }
  449. }).catch((e) => {
  450. ElMessage({ offset: 90, type: 'error', message: `保存失败,${e}` })
  451. }).finally(() => {
  452. loading.save = false
  453. })
  454. }
  455. onBeforeUnmount(() => {
  456. clearDraw()
  457. handleInspect(false)
  458. showShapes({
  459. id: 'qjchs_drew_non_editable',
  460. data: null
  461. })
  462. clearResults()
  463. clearAllCubeDetail()
  464. })
  465. </script>
  466. <style lang="scss">
  467. @use '../../../assets/styles/panel-form.scss';
  468. </style>
  469. <style lang="scss" scoped>
  470. .panel-qjchs {
  471. :deep(.p-form) {
  472. .el-form-item__label {
  473. width: 125px;
  474. }
  475. }
  476. }
  477. </style>