| New file |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false" width="80%"> |
| | | <div class="bodyBox"> |
| | | <div class="leftMap ztzf-cesium" id="mapContainer"></div> |
| | | <div class="rightInfo"> |
| | | <div v-if="readonly"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <div>区域名称: {{ formData.areaName }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>区域位置: {{ formatLocation(formData) }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>区域面积: {{ formData.areaSize }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>区域类型: {{ getOptionLabel(formData.areaType, areaTypeOptions) }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>触发条件: {{ formData.triggerCondition }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>响应机制: {{ formData.responseMechanism }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>管控级别: {{ getOptionLabel(formData.controlLevel, controlLevelOptions) }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>关联派出所: {{ formData.policeStationName }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div>可飞行时间段: {{ formatFlyDate(formData) }}</div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | <el-form v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly" label-width="100px"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="区域名称" prop="areaName"> |
| | | <el-input v-model="formData.areaName" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="区域位置" prop="longitude"> |
| | | <div>{{ formatLocation(formData) }}</div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="区域面积" prop="areaSize"> |
| | | {{ formData.areaSize }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="区域类型" prop="areaType"> |
| | | <el-select v-model="formData.areaType" placeholder="请选择" clearable> |
| | | <el-option |
| | | v-for="item in areaTypeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="响应机制" prop="responseMechanism"> |
| | | <el-input v-model="formData.responseMechanism" maxlength="200" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="触发条件" prop="triggerCondition"> |
| | | <el-input v-model="formData.triggerCondition" maxlength="200" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="管控级别" prop="controlLevel"> |
| | | <el-select v-model="formData.controlLevel" placeholder="请选择" clearable> |
| | | <el-option |
| | | v-for="item in controlLevelOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="可飞行时段" prop="flyDateRange"> |
| | | <el-date-picker |
| | | v-model="flyDateRange" |
| | | type="datetimerange" |
| | | range-separator="至" |
| | | start-placeholder="开始时间" |
| | | end-placeholder="结束时间" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="关联派出所" prop="policeStationId"> |
| | | <el-select v-model="formData.policeStationId" placeholder="请选择" clearable> |
| | | <el-option |
| | | v-for="item in policeStationOptions" |
| | | :key="item.id" |
| | | :label="item.stationName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="关联设备" prop="deviceIds"> |
| | | <el-select v-model="deviceIdList" multiple collapse-tags placeholder="请选择" clearable> |
| | | <el-option v-for="item in deviceOptions" :key="item.id" :label="item.deviceName" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button> |
| | | <el-button v-if="!readonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit"> |
| | | 确定 |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, nextTick, onMounted, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { fwAreaDivideDetailApi, fwAreaDivideSubmitApi, fwDevicePageApi, fwPoliceStationPageApi } from './partitionApi' |
| | | import { fieldRules } from '@ztzf/utils' |
| | | import { PublicCesium } from '@/utils/cesium/publicCesium' |
| | | import { DrawPolygon } from '@/utils/cesium/DrawPolygon' |
| | | import { cartesian3Convert } from '@/utils/cesium/mapUtil' |
| | | import * as turf from '@turf/turf' |
| | | import * as Cesium from 'cesium' |
| | | |
| | | const initForm = () => ({ |
| | | areaName: '', // 区域名称 |
| | | areaSize: null, // 区域面积 |
| | | areaType: '', // 区域类型 |
| | | controlLevel: '', // 管控级别 |
| | | deviceIds: '', // 关联设备ID |
| | | flyDateEnd: '', // 可飞行结束 |
| | | flyDateStart: '', // 可飞行开始 |
| | | latitude: null, // 区域中心纬度 |
| | | longitude: null, // 区域中心经度 |
| | | policeStationId: '', // 关联派出所id |
| | | responseMechanism: '', // 响应机制 |
| | | triggerCondition: '', // 触发条件 |
| | | }) |
| | | |
| | | const areaTypeOptions = [ |
| | | { label: '报警区', value: '报警区' }, |
| | | { label: '重点管制区', value: '重点管制区' }, |
| | | ] |
| | | const controlLevelOptions = [ |
| | | { label: '日常', value: '日常' }, |
| | | { label: '重点', value: '重点' }, |
| | | { label: '严密', value: '严密' }, |
| | | ] |
| | | |
| | | const emit = defineEmits(['success']) |
| | | const formRef = ref(null) // 表单实例 |
| | | const formData = ref(initForm()) // 表单数据 |
| | | const visible = defineModel() // 弹框显隐 |
| | | const dialogMode = ref('add') // 弹框模式 |
| | | const submitting = ref(false) // 提交中 |
| | | const readonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' }) |
| | | const flyDateRange = ref([]) // 飞行时间 |
| | | const deviceIdList = ref([]) // 关联设备ID |
| | | const policeStationOptions = ref([]) // 关联派出所 |
| | | const deviceOptions = ref([]) // 关联设备 |
| | | let viewer |
| | | let drawPolygonExample |
| | | let pointList = [] |
| | | |
| | | const rules = { |
| | | areaName: fieldRules(true, 50), |
| | | areaType: fieldRules(true, 0), |
| | | controlLevel: fieldRules(true, 0), |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 提交新增/编辑 |
| | | async function handleSubmit() { |
| | | const isValid = await formRef.value?.validate().catch(() => false) |
| | | if (!isValid) return |
| | | submitting.value = true |
| | | try { |
| | | const [start, end] = flyDateRange.value ?? [] |
| | | formData.value.flyDateStart = start || '' |
| | | formData.value.flyDateEnd = end || '' |
| | | formData.value.deviceIds = deviceIdList.value.length ? deviceIdList.value.join(',') : '' |
| | | let str = [...pointList, pointList[0]].map(item => `${item.longitude} ${item.latitude}`).join(',') |
| | | formData.value.geom = `POLYGON((${str})) | 4326` |
| | | await fwAreaDivideSubmitApi(formData.value) |
| | | ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功') |
| | | visible.value = false |
| | | emit('success') |
| | | } finally { |
| | | submitting.value = false |
| | | } |
| | | } |
| | | |
| | | // 加载详情 |
| | | async function loadDetail() { |
| | | if (!formData.value.id) return |
| | | const res = await fwAreaDivideDetailApi({ id: formData.value.id }) |
| | | formData.value = res?.data?.data ?? initForm() |
| | | flyDateRange.value = [formData.value.flyDateStart, formData.value.flyDateEnd].filter(Boolean) |
| | | deviceIdList.value = formData.value.deviceIds ? formData.value.deviceIds.split(',') : [] |
| | | } |
| | | |
| | | // 格式化区域位置 |
| | | function formatLocation(row) { |
| | | if (row?.longitude == null || row?.latitude == null) return '' |
| | | return `${row.longitude}, ${row.latitude}` |
| | | } |
| | | |
| | | // 格式化可飞行时间段 |
| | | function formatFlyDate(row) { |
| | | if (!row?.flyDateStart && !row?.flyDateEnd) return '' |
| | | return `${row.flyDateStart || '-'} ~ ${row.flyDateEnd || '-'}` |
| | | } |
| | | |
| | | // 获取选项文本 |
| | | function getOptionLabel(value, options) { |
| | | const match = options.find(item => item.value === value) |
| | | return match?.label ?? value ?? '' |
| | | } |
| | | |
| | | // 初始化地图实例 |
| | | function initMap() { |
| | | const publicCesiumInstance = new PublicCesium({ |
| | | dom: 'mapContainer', |
| | | flatMode: false, |
| | | terrain: true, |
| | | layerMode: 4, |
| | | boundary: false, |
| | | }) |
| | | viewer = publicCesiumInstance.getViewer() |
| | | } |
| | | |
| | | // 绘制完成回调 |
| | | const drawFinished = async data => { |
| | | pointList = _.cloneDeep(data).map(item => { |
| | | const val = cartesian3Convert(item, viewer) |
| | | return { ...val, lng: val.longitude, lat: val.latitude } |
| | | }) |
| | | const polygon = turf.polygon([ |
| | | [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[0].latitude]], |
| | | ]) |
| | | const center = turf.centerOfMass(polygon) |
| | | formData.value.areaSize = _.round(turf.area(polygon), 2) |
| | | formData.value.longitude = center.geometry.coordinates[0] |
| | | formData.value.latitude = center.geometry.coordinates[1] |
| | | } |
| | | |
| | | // 新增模式绘制 |
| | | function addPolygon() { |
| | | drawPolygonExample = new DrawPolygon(viewer) |
| | | drawPolygonExample.initHandler(viewer) |
| | | drawPolygonExample.subscribe('getPolygonPositions', drawFinished) |
| | | } |
| | | |
| | | // 解析经纬度 |
| | | function getLonLat(str) { |
| | | if (!str) return [] |
| | | return str |
| | | .replace('POLYGON((', '') |
| | | .replace(')) | 4326', '') |
| | | .split(',') |
| | | .map(pair => { |
| | | const [lat, lon] = pair.trim().split(' ').map(Number) |
| | | return [lon, lat] |
| | | }) |
| | | } |
| | | |
| | | // 编辑面 |
| | | function editPolygon() { |
| | | if (!formData.value?.geom) return |
| | | const grouped = getLonLat(formData.value.geom) |
| | | grouped.pop() |
| | | pointList = grouped.map(item => ({ longitude: item[0], latitude: item[1] })) |
| | | drawPolygonExample?.destroy() |
| | | drawPolygonExample = new DrawPolygon(viewer) |
| | | drawPolygonExample.initPolygon( |
| | | viewer, |
| | | grouped.map(item => ({ lng: item[0], lat: item[1] })) |
| | | ) |
| | | drawPolygonExample.subscribe('getPolygonPositions', drawFinished) |
| | | } |
| | | |
| | | // 查看面 |
| | | function viewPolygon() { |
| | | if (!formData.value?.geom) return |
| | | const grouped = getLonLat(formData.value.geom) |
| | | grouped.pop() |
| | | pointList = grouped.map(item => ({ longitude: item[0], latitude: item[1] })) |
| | | const result = grouped.flat().flat() |
| | | const mian = viewer.entities?.add({ |
| | | customType: 'control_group', |
| | | position: Cesium.Cartesian3.fromDegrees(result[0], result[1]), |
| | | polyline: { |
| | | positions: Cesium.Cartesian3.fromDegreesArray(result), |
| | | clampToGround: true, |
| | | width: 3, |
| | | material: Cesium.Color.RED, |
| | | }, |
| | | }) |
| | | viewer.flyTo(mian) |
| | | } |
| | | |
| | | // 获取派出所列表 |
| | | async function getPoliceStationList() { |
| | | const res = await fwPoliceStationPageApi({ current: 1, size: 9999 }) |
| | | policeStationOptions.value = res?.data?.data?.records ?? [] |
| | | } |
| | | |
| | | // 获取设备列表 |
| | | async function getDeviceList() { |
| | | const res = await fwDevicePageApi({ current: 1, size: 9999 }) |
| | | deviceOptions.value = res?.data?.data?.records ?? [] |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | formData.value = dialogMode.value === 'add' ? initForm() : row |
| | | await nextTick() |
| | | initMap() |
| | | if (dialogMode.value === 'add') { |
| | | addPolygon() |
| | | } else if (dialogMode.value === 'edit') { |
| | | await loadDetail() |
| | | editPolygon() |
| | | } else { |
| | | await loadDetail() |
| | | viewPolygon() |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getPoliceStationList() |
| | | getDeviceList() |
| | | }) |
| | | |
| | | defineExpose({ |
| | | open, |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .bodyBox { |
| | | display: flex; |
| | | height: 800px; |
| | | .leftMap { |
| | | width: 70%; |
| | | height: 100%; |
| | | } |
| | | .rightInfo { |
| | | width: 30%; |
| | | height: 100%; |
| | | overflow: auto; |
| | | } |
| | | } |
| | | </style> |