applications/mobile-web-view/src/appComponents/LeafletMap/indexWX.vue
New file @@ -0,0 +1,607 @@ <template> <div class="page-container"> <div class="map" id="map"></div> <!-- <div class="layer-btn" @click="layerChangePopupShow = true" v-show="layerShow"> <van-image width="20" height="20" :src="layerIcon" /> <div class="label">图层</div> </div> <div class="location-btn" @click="setMapLocation" v-show="locationShow"> <van-image width="20" height="20" :src="locationIcon" /> <div class="label">定位</div> </div> --> <slot name="searchBar"></slot> <van-popup v-model:show="layerChangePopupShow" position="bottom" round> <div class="popup-container"> <div class="category-content"> <div class="header"> <div class="title">底图切换</div> </div> <div class="content"> <div class="layer-box" :class="{ on: setSelectMapLayerKey === item.key }" v-for="(item, ind) in layers" :key="ind" @click="setMapLayer(item)" > <div class="image"> <van-image :src="item.src" width="100%" height="100%"></van-image> </div> <div class="label">{{ item.name }}</div> </div> </div> </div> </div> </van-popup> </div> </template> <script setup> import * as turf from '@turf/turf' import addressLocationIcon from '@/appDataSource/leafletMapIcon/address-location.svg' import droneIcon from '@/appDataSource/leafletMapIcon/drone-icon.svg' import userLocationIcon from '@/appDataSource/leafletMapIcon/user-location.svg' import layerIcon from '@/appDataSource/leafletMapIcon/layer-icon.svg' import locationIcon from '@/appDataSource/leafletMapIcon/location-icon.svg' import { useStore } from 'vuex' import { showToast } from 'vant' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import sl from '@/appDataSource/leafletMapIcon/sl.svg' import yx from '@/appDataSource/leafletMapIcon/yx.svg' import EventBus from '@/utils/eventBus' import { getEventImage } from '@/utils/stateToImageMap/event' const emit = defineEmits(['location-selected']) const { weatherShow, layerShow, locationShow, createWorkShow } = defineProps({ weatherShow: { type: Boolean, default: false, }, layerShow: { type: Boolean, default: false, }, locationShow: { type: Boolean, default: false, }, createWorkShow: { type: Boolean, default: false, }, }) const isProd = import.meta.env.VITE_APP_ENV === 'production' import { basemapLayer0, basemapLayer1, basemapLayer2, basemapLayer3 } from '@/const/leafletConst' const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1]) const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3]) let map = null const isMapReady = ref(false) const layers = [ { src: sl, name: '矢量', key: 1, map: basemap0, }, { src: yx, name: '影像', key: 2, map: basemap1, }, ] const layerChangePopupShow = ref(false) const setSelectMapLayerKey = ref(1) // 选择的地图图层 let droneMarkersLayer = null let eventMarkersLayer = null let addressMarkersLayer = null let mapLayerSourceLayer = null // 空域信息 const initMap = () => { if (map) return map = L.map('map', { preferCanvas: true, crs: L.CRS.EPSG4326, zoomControl: false, attributionControl: false, doubleClickZoom: false, editable: true, //绘制控件 }).setView([27.117445, 114.984917], 10) map.whenReady(() => { isMapReady.value = true }) droneMarkersLayer = L.layerGroup([], { pane: 'drones' }).addTo(map) // 创建一个标注层,便于管理和移除 eventMarkersLayer = L.layerGroup([], { pane: 'events' }).addTo(map) // 创建一个标注层,便于管理和移除 addressMarkersLayer = L.layerGroup([], { pane: 'addresses' }).addTo(map) // 创建一个标注层,便于管理和移除 // 空域信息 mapLayerSourceLayer = L.layerGroup([], { pane: 'mapLayerSource' }).addTo(map) map.on('zoomend', function () { var currentZoom = map.getZoom() if (currentZoom >= 11) { eventMarkersLayer.addTo(map) } else { eventMarkersLayer.remove() } }) if (createWorkShow) { getMapLocation() } } const getCurMap = () => { return map } const setMapLayer = item => { map.removeLayer(layers.find(i => i.key === setSelectMapLayerKey.value).map) map.addLayer(item.map) setSelectMapLayerKey.value = item.key } let locationFlag = false let watchId = null let userLocationMarker = null const setMapLocation = () => { locationFlag = true // 在WebView加载的网页中 // 检查浏览器是否支持 geolocation if (navigator.geolocation) { if (watchId) { // 停止监听位置 navigator.geolocation.clearWatch(watchId) } // 开始持续获取用户的位置 watchId = navigator.geolocation.watchPosition( function (position) { // 成功获取位置信息时回调 const lat = position.coords.latitude // 纬度 const lng = position.coords.longitude // 经度 if (locationFlag) { if (userLocationMarker) { userLocationMarker.setLatLng([lat, lng]) } else { userLocationMarker = L.marker([lat, lng], { icon: L.icon({ iconUrl: userLocationIcon, // 图片路径 iconSize: [24, 24], // 图标尺寸 iconAnchor: [12, 12], // 锚点位置 }), }).addTo(map) } mapSetView({ lat, lng, }) locationFlag = false return } if (userLocationMarker) { userLocationMarker.setLatLng([lat, lng]) } }, function (error) { // 如果获取位置失败,执行回调 console.error('Error occurred: ' + error.message) }, { enableHighAccuracy: true, // 尝试获取更高精度的定位 timeout: 10000, // 如果10秒内没有获取到位置,就中止 maximumAge: 0, // 始终请求新的定位,不使用缓存 } ) } else { console.log('该浏览器不支持地理定位功能。') } } let lastLocationMarker = null const getMapLocation = () => { map.off('click') map.on('click', function (e) { const { lat, lng } = e.latlng if (lastLocationMarker) { map.removeLayer(lastLocationMarker) } lastLocationMarker = L.marker([lat, lng]).addTo(map) emit('location-selected', { latitude: lat, longitude: lng, }) }) } const mapSetView = data => { const { lat, lng, zoom = 16 } = data if (!map) return map.setView([lat, lng], zoom, { animate: false, // 使用动画过渡 }) } const mapAddMarker = data => { if (!map) return const { lat, lng, zoom = 16, name = '', customClassName = '' } = data mapSetView(data) // 创建带有图标和文字的 divIcon let droneHtmlIcon = L.divIcon({ html: ` <div class="content ${customClassName}"> <img src="${addressLocationIcon}"> <span> ${name} </span> </div> `, className: 'ztzf-address-custom-icon', // 可选,用于添加CSS类 iconSize: [24, 38], // 设置图标大小 iconAnchor: [12, 19], }) L.marker([lat, lng], { icon: droneHtmlIcon }).addTo(addressMarkersLayer) } // 吉安项目-空域信息 // 统一的飞行函数 function flyVisual(coordinates) { // 确保map对象存在且坐标有效 if (!map || !coordinates || !coordinates.length) return; try { // 改进坐标格式判断 let isSinglePoint = false; let latLng; let bounds; // 处理不同格式的坐标输入 if (Array.isArray(coordinates[0])) { if (coordinates[0].length === 2) { // 格式: [[lng, lat]] - 单个点 isSinglePoint = true; latLng = [coordinates[0][1], coordinates[0][0]]; // 转换为 [lat, lng] } else if (coordinates[0].length > 2 && Array.isArray(coordinates[0][0])) { // 格式: [[[lng, lat], [lng, lat], ...]] - 多边形 const polygonCoords = coordinates[0].map(coord => [coord[1], coord[0]]); bounds = L.latLngBounds(polygonCoords); } else { // 格式: [[lng, lat], [lng, lat], ...] - 多个点或线 if (coordinates.length === 1) { // 单个点 isSinglePoint = true; latLng = [coordinates[0][1], coordinates[0][0]]; } else { // 多个点 const leafletCoords = coordinates.map(coord => [coord[1], coord[0]]); bounds = L.latLngBounds(leafletCoords); } } } else if (coordinates.length === 2) { // 格式: [lng, lat] - 单个点 isSinglePoint = true; latLng = [coordinates[1], coordinates[0]]; } if (isSinglePoint && latLng) { // 单个坐标点 map.flyTo(latLng, 10, { duration: 1, easeLinearity: 0.2 }); } else if (bounds) { // 多边形或多个点 map.flyToBounds(bounds, { padding: [30, 30], duration: 2, maxZoom: 13 }); } } catch (error) { console.error('flyVisual 函数执行错误:', error); } } // 点击单个飞行 function signFlyToMarker(item) { const positionsData = JSON.parse(item.geoJson); const coordinates = positionsData.coordinates; // 更全面的空值检查 let hasValidCoordinates = false; // 检查不同格式的坐标数据 if (Array.isArray(coordinates)) { if (coordinates.length > 0) { // 检查是否有有效的坐标点 if (Array.isArray(coordinates[0])) { if (coordinates[0].length === 2 || coordinates[0].length === 3) { // 格式: [lng, lat] 或 [lng, lat, alt] hasValidCoordinates = true; } else if (Array.isArray(coordinates[0][0])) { // 格式: [[lng, lat], [lng, lat], ...] hasValidCoordinates = coordinates[0].length > 0; } else if (Array.isArray(coordinates[0][0][0])) { // 格式: [[[lng, lat], [lng, lat], ...]] hasValidCoordinates = coordinates[0][0].length > 0; } } } } if (!hasValidCoordinates) { console.log('没有有效的坐标数据,显示提示'); return showToast('暂无区域数据'); } // 单个位置飞行 flyVisual(coordinates); } // 清空所有空域信息 function mapClearAirMarker() { if (!map) return // 清空所有图层 if (mapLayerSourceLayer) { mapLayerSourceLayer.clearLayers(); } } function mapAddAirMarker(data) { // 清空所有图层 mapClearAirMarker() data .filter(item => item.geoJson) .forEach(item => { const parseParsed = JSON.parse(item.geoJson); // 转为数组 if (parseParsed.type !== 'Polygon') return const parsed = parseParsed.coordinates[0] const grouped = parsed.flat(); // 再展开 // 3. 计算多边形质心 const turfPolygon = turf.polygon([parsed]); const center = turf.centerOfMass(turfPolygon); const centerLatLng = [center.geometry.coordinates[1], center.geometry.coordinates[0]]; const labelMarker = L.marker(centerLatLng, { icon: L.divIcon({ className: 'polygon-label', html: ` <div style=" color: ${item.airspaceType === '0' ? '#008013' : '#ff0000'}; font-weight: bold; font-size: 12px; font-family: 'Source Han Sans CN', 'Source Han Sans CN'; white-space: nowrap; "> ${item.airspaceType === '1' ? '禁飞区' : item.airspaceType === '2' ? '危险区' : item.airspaceType === '3' ? '限制区' : item.airspaceType === '0' ? '适飞区' : '未知'} </div> `, iconSize: null, // 让div决定大小 iconAnchor: null // 不需要锚点,用CSS transform居中 }), interactive: false, // 标签不参与交互 // zIndexOffset: 1000 // 确保标签在最上层 }); // 将坐标数组转换为 Leaflet 格式 [lat, lng, lat, lng...] -> [[lat, lng], [lat, lng]...] const latLngs = []; for (let i = 0; i < grouped.length; i += 2) { latLngs.push([grouped[i + 1], grouped[i]]); // 注意:Leaflet 是 [lat, lng] 顺序 } // 创建多边形(填充区域) const polygon = L.polygon(latLngs, { color: item.airspaceType === '0' ? '#00ff06' : '#ff0000', fillColor: item.airspaceType === '0' ? '#00ff06' : '#ff0000', fillOpacity: 0.5, weight: 0, // 隐藏多边形边框,使用折线作为边框 }); // 创建折线(边框) const polyline = L.polyline([...latLngs, latLngs[0]], { // 闭合多边形 color: item.airspaceType === '0' ? '#00ff06' : '#ff0000', weight: 1.5, opacity: 1, fill: false }); // 将两个图层组合在一起 const group = L.featureGroup([polygon, polyline, labelMarker]); // 添加到图层组 mapLayerSourceLayer.addLayer(group); }); } const mapClearMarker = () => { addressMarkersLayer.clearLayers() } // 天气 const store = useStore() const selectedAreaCode = computed(() => store.state.user.selectedAreaCode) onMounted(async () => { await nextTick() initMap() map.addLayer(layers[0].map) EventBus.on('mapSetView', mapSetView) EventBus.on('mapAddMarker', mapAddMarker) EventBus.on('mapAddAirMarker', mapAddAirMarker) EventBus.on('signFlyToMarker', signFlyToMarker) EventBus.on('mapClearAirMarker', mapClearAirMarker) EventBus.on('mapClearMarker', mapClearMarker) // L.circle([24, 112], { // color: 'red', // 边框颜色 // fillColor: '#f03', // 填充颜色 // fillOpacity: 0.5, // 填充透明度 // radius: 5000, // 半径,单位是米 // }).addTo(map) }) onUnmounted(() => { EventBus.off('mapSetView', mapSetView) EventBus.off('mapAddMarker', mapAddMarker) EventBus.off('mapAddAirMarker', mapAddAirMarker) EventBus.off('signFlyToMarker', signFlyToMarker) EventBus.off('mapClearAirMarker', mapClearAirMarker) EventBus.off('mapClearMarker', mapClearMarker) }) </script> <style lang="scss" scoped> .page-container { position: relative; width: 100%; height: 100%; .map { width: 100%; height: 100%; } .location-btn, .layer-btn { display: flex; flex-direction: column; justify-content: center; align-items: center; position: absolute; right: 8px; width: 40px; height: 40px; background: #ffffff; box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.2); border-radius: 6px 6px 6px 6px; z-index: 996; .label { font-size: 10px; } } .layer-btn { bottom: 130px; } .location-btn { bottom: 80px; } } .popup-container { height: 300px; overflow-y: auto; padding: 10px 22px; background: #fff; border-radius: 12px 12px 0 0; .category-content { margin-bottom: 10px; .header { margin-bottom: 10px; height: 20px; font-family: Source Han Sans CN, Source Han Sans CN; font-weight: bold; font-size: 16px; color: #222324; line-height: 20px; text-align: left; font-style: normal; text-transform: none; } .base-header { margin-top: 10px; } .content { //padding: 12px; display: flex; background: #fff; border-radius: 16px; .layer-box { margin-left: calc((100% / 3 - 84px) * 3 / 2); &:first-child { margin-left: 0; } &.on { .image { position: relative; &::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 4px 4px 4px 4px; border: 4px solid #1d6fe9; z-index: 1; box-sizing: border-box; } } .label { color: #1d6fe9; } } .image { width: 84px; height: 82px; border-radius: 4px 4px 4px 4px; overflow: hidden; box-sizing: border-box; border: none; } .label { margin-top: 6px; height: 20px; font-family: Source Han Sans CN, Source Han Sans CN; font-weight: 400; font-size: 14px; color: #222324; line-height: 20px; text-align: center; font-style: normal; text-transform: none; } } .layer-box:nth-child(3n + 1) { margin-left: 0; } } } } </style> applications/task-work-order/src/styles/common/cockpit.scss
@@ -1,5 +1,6 @@ //变量 $gd-placeholder: #86909C; $form-bg:#F2F3F5; .gd-table { height: 0; @@ -77,11 +78,10 @@ } } .gd-input { .el-input__wrapper,.el-textarea__inner { padding: 0 8px !important; background: #F2F3F5; background: $form-bg; box-shadow: none !important; border: none; } @@ -107,7 +107,7 @@ .el-select__wrapper { padding: 0 8px; background: #DDE2ED; background: $form-bg; box-shadow: none !important; border: none; width: 100%; @@ -141,7 +141,7 @@ } .gd-tree-select-popper { background: #DDE2ED !important; background: $form-bg !important; border: none !important; .el-popper__arrow::before { @@ -220,7 +220,7 @@ .gd-date-picker { box-shadow: none !important; background: #DDE2ED; background: $form-bg; border: none; border-radius: 5px; box-sizing: border-box !important; @@ -562,94 +562,7 @@ .el-form-item__content { display: flex; align-items: center; .el-input, .el-input-number { width: 0; flex: 1; } .el-cascader { width: 0; flex: 1; display: flex; align-items: center; border: none; .el-input__wrapper { box-shadow: none !important; } } .el-button { color: #fff; background: #DDE2ED !important; border: none; box-shadow: none; } .gd-date-picker { background: #DDE2ED; } .gd-date-picker-popper { background: #DDE2ED !important; .el-picker-panel { .el-picker-panel__body-wrapper { .el-picker-panel__body { background: #DDE2ED !important; .el-date-picker__time-header { .el-date-picker__editor-wrap { .el-time-panel { background: #DDE2ED !important; // 将 time-picker-popper 的样式移到这里 .el-time-panel__header { background: #DDE2ED !important; } } } } } } } .el-popper__arrow { &::before { background: #DDE2ED !important; border: 1px solid #DDE2ED !important; box-sizing: border-box; } } } } .el-select { .el-select__wrapper { background: #F2F3F5; .el-select__suffix { .el-icon { color: #4E5969; } } } .el-select__placeholder { &.is-transparent { color: #86909C !important; } &.el-select__selected-item { color: #383874; } } } } } } @@ -811,7 +724,6 @@ .gd-select { .el-select__wrapper { background: #DDE2ED; width: 200px; height: 36px; } @@ -822,37 +734,30 @@ height: 36px; .el-input__wrapper { background: #DDE2ED; } &.is-disabled { .el-input__wrapper { background-color: #DDE2ED; } } } .gd-date-picker { background: #DDE2ED; } .gd-date-picker-popper { background: #DDE2ED !important; .el-picker-panel { .el-picker-panel__body-wrapper { .el-picker-panel__body { background: #DDE2ED !important; .el-date-picker__time-header { .el-date-picker__editor-wrap { .el-time-panel { background: #DDE2ED !important; // 将 time-picker-popper 的样式移到这里 .el-time-panel__header { background: #DDE2ED !important; } } } @@ -917,99 +822,6 @@ .el-button { width: 96px; height: 36px; } } .gd-dialog-form { .el-form-item { margin-left: 20px; } .el-form-item__label { font-family: Source Han Sans CN, Source Han Sans CN; font-weight: 500; font-size: 14px; color: #C5C9D6; font-style: normal; text-transform: none; } .gd-input { .el-input__wrapper { background: #DDE2ED; } &.is-disabled { .el-input__wrapper { background-color: #DDE2ED; } } } .gd-date-picker { background: #DDE2ED; } .gd-date-picker-popper { background: #DDE2ED !important; .el-picker-panel { .el-picker-panel__body-wrapper { .el-picker-panel__body { background: #DDE2ED !important; .el-date-picker__time-header { .el-date-picker__editor-wrap { .el-time-panel { background: #DDE2ED !important; // 将 time-picker-popper 的样式移到这里 .el-time-panel__header { background: #DDE2ED !important; } } } } } } } .el-popper__arrow { &::before { background: #DDE2ED !important; border: 1px solid #DDE2ED !important; box-sizing: border-box; } } } .gd-search-actions { .el-button { width: 96px; background: transparent !important; border-radius: 4px 4px 4px 4px; border: 1px solid rgba(255, 255, 255, 0.3); .el-icon { font-size: 16px; color: #fff; } } } .el-input { width: 160px; } .el-date-editor { width: 226px; } .el-select { .el-select__wrapper { width: 160px; } } } applications/task-work-order/src/views/basicManage/maintainRecord/FormDiaLog.vue
@@ -52,7 +52,7 @@ </div> <el-form class="dialog-form" class="gd-dialog-form" v-else ref="formRef" :model="formData" applications/task-work-order/src/views/orderView/orderDataManage/evaluate/FormDiaLog.vue
@@ -40,7 +40,7 @@ </div> <el-form class="dialog-form" class="gd-dialog-form" v-else ref="formRef" :model="formData" applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/FormDiaLog.vue
@@ -60,7 +60,7 @@ </div> <el-form class="dialog-form" class="gd-dialog-form" v-else ref="formRef" :model="formData" @@ -187,7 +187,6 @@ </el-row> </el-form> <template #footer> <el-button color="#F2F3F5" @click="visible = false">{{ dialogReadonly ? '关闭' : '取消' }}</el-button> <el-button class="save-btn" color="#4C34FF" @@ -198,6 +197,8 @@ > 保存 </el-button> <el-button color="#F2F3F5" :loading="submitting" :disabled="submitting" @click="handleApply">申请</el-button> </template> </el-dialog> </template> @@ -226,6 +227,7 @@ responsibleDeptId: '', dataSource: '', demandInfo: '', demandStatus: '0' }) const dictObj = inject('dictObj') @@ -262,6 +264,7 @@ if (!isValid) return submitting.value = true try { formData.value.demandStatus = '0' await gdSupplyDemandSubmitApi(formData.value) ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功') visible.value = false @@ -271,6 +274,22 @@ } } // 申请提交 async function handleApply() { const isValid = await formRef.value?.validate().catch(() => false) if (!isValid) return submitting.value = true try { formData.value.demandStatus = '1' await gdSupplyDemandSubmitApi(formData.value) ElMessage.success('申请成功') visible.value = false emit('success') } finally { submitting.value = false } } // 加载详情 async function loadDetail() { if (!formData.value.id) return applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/index.vue
@@ -54,9 +54,9 @@ <el-table-column prop="contactPhone" show-overflow-tooltip label="需求联系人电话" /> <el-table-column prop="contactEmail" show-overflow-tooltip label="需求邮箱" /> <el-table-column prop="demandStatus" show-overflow-tooltip label="需求状态"> <!-- <template v-slot="{ row }"> <template v-slot="{ row }"> {{ getDictLabel(row.demandStatus, dictObj.demandStatus) }} </template> --> </template> </el-table-column> <el-table-column prop="updateCycle" show-overflow-tooltip label="更新周期" /> <el-table-column prop="applyBasis" show-overflow-tooltip label="申请依据" /> @@ -196,7 +196,7 @@ // 获取字典 function getDictList() { getDictionaryByCode('requirementType,sharedType,appSceneType').then(res => { getDictionaryByCode('requirementType,sharedType,appSceneType,demandStatus').then(res => { dictObj.value = res.data.data }) } applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
New file @@ -0,0 +1,274 @@ <template> <el-dialog class="gd-dialog" v-model="visible" :title="titleEnum[dialogMode]" @closed="visible = false" destroy-on-close width="1000px" > <div class="detail-container" v-if="dialogReadonly"> <el-row> <el-col :span="12"> <div class="label">工单名称</div> <div class="val">{{ formData.workOrderName }}</div> </el-col> <el-col :span="12"> <div class="label">工单类型</div> <div class="val">{{ getDictLabel(formData.workOrderType, dictObj.workOrderType) }}</div> </el-col> <el-col :span="12"> <div class="label">执行开始时间</div> <div class="val">{{ formData.executeStartTime }}</div> </el-col> <el-col :span="12"> <div class="label">执行结束时间</div> <div class="val">{{ formData.executeEndTime }}</div> </el-col> <el-col :span="12"> <div class="label">设备需求</div> <div class="val">{{ getDictLabel(formData.deviceLoadDemand, dictObj.deviceLoadDemand) }}</div> </el-col> <el-col :span="12"> <div class="label">工单备注</div> <div class="val">{{ formData.remark }}</div> </el-col> <el-col :span="24"> <div class="label">工单经纬度面</div> <div class="val">{{ formData.geom }}</div> </el-col> <el-col :span="24"> <div class="label">推荐设备</div> <div class="val">{{ formData.recommendDeviceIds }}</div> </el-col> </el-row> </div> <el-form class="gd-dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="dialogReadonly" label-width="120px" > <el-row> <el-col :span="12"> <el-form-item label="工单名称" prop="workOrderName"> <el-input class="gd-input" v-model="formData.workOrderName" placeholder="请输入" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="工单类型" prop="workOrderType"> <el-select class="gd-select" popper-class="gd-select-popper" v-model="formData.workOrderType" placeholder="请选择" clearable > <el-option v-for="item in dictObj.workOrderType" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="执行开始时间" prop="executeStartTime"> <el-date-picker class="gd-date-picker" popper-class="gd-date-picker-popper" v-model="formData.executeStartTime" type="datetime" placeholder="请选择" value-format="YYYY-MM-DD HH:mm:ss" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="执行结束时间" prop="executeEndTime"> <el-date-picker class="gd-date-picker" popper-class="gd-date-picker-popper" v-model="formData.executeEndTime" type="datetime" placeholder="请选择" value-format="YYYY-MM-DD HH:mm:ss" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="设备需求" prop="deviceLoadDemand"> <el-select class="gd-select" popper-class="gd-select-popper" v-model="formData.deviceLoadDemand" placeholder="请选择" clearable > <el-option v-for="item in dictObj.deviceLoadDemand" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="工单备注" prop="remark"> <el-input class="gd-input" v-model="formData.remark" placeholder="请输入" clearable /> </el-form-item> </el-col> <el-col :span="24"> <el-form-item label="工单经纬度面" prop="geom"> <el-input class="gd-input" v-model="formData.geom" placeholder="预留输入框" clearable /> </el-form-item> </el-col> <el-col :span="24"> <el-form-item label="推荐设备" prop="recommendDeviceIds"> <el-form-item prop="recommendDeviceIds" label-width="0"> <el-table class="command-data-cockpit-table" ref="deviceTableRef" :data="deviceList" row-key="id" @selection-change="handleDeviceSelectionChange" > <el-table-column type="selection" width="55" :reserve-selection="true" /> <el-table-column prop="deviceName" label="设备名称" /> <el-table-column prop="deviceCode" label="设备编码" /> <el-table-column prop="deviceType" label="设备类型" /> </el-table> </el-form-item> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <el-button color="#F2F3F5" @click="visible = false">{{ dialogReadonly ? '关闭' : '取消' }}</el-button> <el-button class="save-btn" color="#4C34FF" v-if="!dialogReadonly" :loading="submitting" :disabled="submitting" @click="handleSubmit" > 保存 </el-button> </template> </el-dialog> </template> <script setup> import { computed, ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { fieldRules } from '@ztzf/utils' import { getDictLabel } from '@ztzf/utils' import { gdWorkOrderDetailApi, gdWorkOrderSaveApi } from './orderManageApi' import { gdManageDevicePageApi } from './gdManageDeviceApi' // 初始化表单数据 const initForm = () => ({ workOrderName: '', workOrderType: '', executeStartTime: '', executeEndTime: '', deviceLoadDemand: '', remark: '', geom: '', recommendDeviceIds: '', }) const dictObj = inject('dictObj') const deptTree = inject('deptTree') const treeProps = inject('treeProps') const emit = defineEmits(['success']) const formRef = ref(null) // 表单实例 const formData = ref(initForm()) // 表单数据 const visible = defineModel() // 弹框显隐 const dialogMode = ref('add') // 弹框模式 const submitting = ref(false) // 提交中 const dialogReadonly = computed(() => dialogMode.value === 'view') const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' }) const deviceList = ref([]) // 设备列表 const selectedDevices = ref([]) // 选中的设备 const deviceTableRef = ref(null) // 设备表格实例 const rules = { workOrderName: fieldRules(true), workOrderType: fieldRules(true), executeStartTime: fieldRules(true), executeEndTime: fieldRules(true), deviceLoadDemand: fieldRules(true), } // 获取推荐设备列表 async function getDeviceList() { try { const res = await gdManageDevicePageApi({ current: 1, size: 1000 }) deviceList.value = res?.data?.data?.records ?? [] } catch (error) { console.error('获取设备列表失败:', error) } } // 设备选择变化 function handleDeviceSelectionChange(rows) { selectedDevices.value = rows formData.value.recommendDeviceIds = rows.map(item => item.id).join(',') } // 提交新增/编辑 async function handleSubmit() { const isValid = await formRef.value?.validate().catch(() => false) if (!isValid) return submitting.value = true try { await gdWorkOrderSaveApi(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 gdWorkOrderDetailApi({ id: formData.value.id }) formData.value = res?.data?.data ?? {} // 回显选中的设备 if (formData.value.recommendDeviceIds) { const deviceIds = formData.value.recommendDeviceIds.split(',').map(id => parseInt(id)) nextTick(() => { deviceList.value.forEach(device => { if (deviceIds.includes(device.id)) { deviceTableRef.value?.toggleRowSelection(device, true) } }) }) } } // 打开弹框 async function open({ mode = 'add', row } = {}) { dialogMode.value = mode formData.value = dialogMode.value === 'add' ? initForm() : row await getDeviceList() if (dialogMode.value !== 'add') { await loadDetail() } } defineExpose({ open }) </script> applications/task-work-order/src/views/orderView/orderManage/orderManage/gdManageDeviceApi.js
New file @@ -0,0 +1,37 @@ import request from '@/axios' // 详情 export const gdManageDeviceDetailApi = params => { return request({ url: `/drone-gd/workOrder/gdManageDevice/detail`, method: 'get', params, }) } // 分页 export const gdManageDevicePageApi = params => { return request({ url: `/drone-gd/workOrder/gdManageDevice/page`, method: 'get', params, }) } // 新增或修改 export const gdManageDeviceSubmitApi = data => { return request({ url: `/drone-gd/workOrder/gdManageDevice/submit`, method: 'post', data, }) } // 逻辑删除 export const gdManageDeviceRemoveApi = params => { return request({ url: `/drone-gd/workOrder/gdManageDevice/remove`, method: 'post', params, }) } applications/task-work-order/src/views/orderView/orderManage/orderManage/index.vue
@@ -1,9 +1,249 @@ <template> <basic-container> 工单管理 </basic-container> <basic-container> <el-form ref="queryParamsRef" :model="searchParams" class="gd-search-form"> <el-form-item label="工单名称" prop="workOrderName"> <el-input class="gd-input" v-model="searchParams.workOrderName" placeholder="请输入" clearable @clear="handleSearch" /> </el-form-item> <el-form-item label="工单类型" prop="workOrderType"> <el-select class="gd-select" popper-class="gd-select-popper" v-model="searchParams.workOrderType" placeholder="请选择" clearable @change="handleSearch" > <el-option v-for="item in dictObj.workOrderType" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" /> </el-select> </el-form-item> <el-form-item label="工单状态" prop="workOrderStatus"> <el-select class="gd-select" popper-class="gd-select-popper" v-model="searchParams.workOrderStatus" placeholder="请选择" clearable @change="handleSearch" > <el-option v-for="item in dictObj.workOrderStatus" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" /> </el-select> </el-form-item> <el-form-item label="执行时间" prop="executeTime"> <el-date-picker class="gd-date-picker" popper-class="gd-date-picker-popper" v-model="executeTime" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" @change="handleSearch" /> </el-form-item> <el-form-item class="gd-search-actions"> <el-button :icon="RefreshRight" @click="resetForm"></el-button> <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button> </el-form-item> </el-form> <div class="gd-table-toolbar"> <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">新增</el-button> <el-button :icon="Delete" color="#4C34FF" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button> </div> <div class="gd-table-container" v-loading="loading"> <div class="gd-table-content gd-table-content-bg"> <el-table class="gd-table" :data="list" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="46" /> <el-table-column prop="workOrderName" show-overflow-tooltip label="工单名称" /> <el-table-column prop="workOrderCode" show-overflow-tooltip label="工单编号" /> <el-table-column prop="workOrderType" show-overflow-tooltip label="工单类型"> <template v-slot="{ row }"> {{ getDictLabel(row.workOrderType, dictObj.workOrderType) }} </template> </el-table-column> <el-table-column prop="status" show-overflow-tooltip label="阶段状态" /> <el-table-column prop="workOrderStatus" show-overflow-tooltip label="工单状态"> <template v-slot="{ row }"> {{ getDictLabel(row.workOrderStatus, dictObj.workOrderStatus) }} </template> </el-table-column> <el-table-column prop="executeTime" show-overflow-tooltip label="执行时间范围"> <template v-slot="{ row }">{{ row.executeStartTime }} ~ {{ row.executeEndTime }}</template> </el-table-column> <el-table-column prop="serviceParty" show-overflow-tooltip label="服务方名称" /> <el-table-column prop="createDeptName" show-overflow-tooltip label="需求方名称" /> <el-table-column prop="createTime" show-overflow-tooltip label="工单创建时间" /> <el-table-column label="操作" class-name="operation-btns"> <template v-slot="{ row }"> <el-link @click="openForm('view', row)">查看</el-link> <el-link @click="openForm('edit', row)">编辑</el-link> <el-link @click="handleDelete(row)">删除</el-link> </template> </el-table-column> </el-table> </div> <div class="gd-pagination-parent"> <el-pagination popper-class="gd-select-popper" v-model:current-page="searchParams.current" v-model:page-size="searchParams.size" layout="total, prev, pager, next, sizes" :total="total" @change="getList" /> </div> </div> <FormDiaLog ref="dialogRef" @success="getList" v-if="dialogVisible" v-model="dialogVisible" /> </basic-container> </template> <script setup> import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue' import { onMounted, ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getDictionaryByCode } from '@/api/system/dictbiz' import { getDeptTree } from '@/api/system/dept' import { getDictLabel } from '@ztzf/utils' import FormDiaLog from './FormDiaLog.vue' import { gdWorkOrderPageApi, gdWorkOrderRemoveApi } from './orderManageApi' // 初始化查询参数 const initSearchParams = () => ({ workOrderName: '', // 工单名称 workOrderType: '', // 工单类型 workOrderStatus: '', // 工单状态 executeStartTime: '', // 执行开始时间 executeEndTime: '', // 执行结束时间 current: 1, // 当前页 size: 10, // 每页大小 }) const searchParams = ref(initSearchParams()) // 查询参数 const executeTime = ref([]) // 执行时间范围 const total = ref(0) // 总条数 const loading = ref(true) // 列表加载中 const list = ref([]) // 列表数据 const selectedIds = ref([]) // 勾选的ID列表 const queryParamsRef = ref(null) // 查询表单实例 const dialogRef = ref(null) // 弹框实例 const dialogVisible = ref(false) const dictObj = ref({ deviceLoadDemand: [], // 设备负载需求 workOrderType: [], // 工单类型 workOrderStatus: [], // 工单状态 }) provide('dictObj', dictObj) const deptTree = ref([]) // 部门树 const treeProps = { label: 'name', children: 'children', } provide('deptTree', deptTree) provide('treeProps', treeProps) // 获取列表 async function getList() { loading.value = true try { const res = await gdWorkOrderPageApi(searchParams.value) list.value = res?.data?.data?.records ?? [] total.value = res?.data?.data?.total ?? 0 } finally { loading.value = false } } // 查询 function handleSearch() { // 处理执行时间范围 if (executeTime.value && executeTime.value.length === 2) { searchParams.value.executeStartTime = executeTime.value[0] searchParams.value.executeEndTime = executeTime.value[1] } else { searchParams.value.executeStartTime = '' searchParams.value.executeEndTime = '' } searchParams.value.current = 1 getList() } // 重置查询 function resetForm() { queryParamsRef.value?.resetFields() executeTime.value = [] searchParams.value.current = 1 getList() } // 新增/编辑/查看 弹框 function openForm(mode, row) { dialogVisible.value = true nextTick(() => { dialogRef.value?.open({ mode, row }) }) } // 删除 async function handleDelete(row) { const tips = row ? '该条' : '选中的项' await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', { type: 'warning', customClass: 'gd-confirm-custom', confirmButtonClass: 'gd-confirm-button', cancelButtonClass: 'gd-confirm-cancel-button', }) const ids = row ? row.id : selectedIds.value.join(',') await gdWorkOrderRemoveApi({ ids }) ElMessage.success('删除成功') selectedIds.value = [] getList() } // 勾选值设置 function handleSelectionChange(rows) { selectedIds.value = rows.map(item => item.id) } // 获取字典 function getDictList() { getDictionaryByCode('deviceLoadDemand,workOrderType,workOrderStatus').then(res => { dictObj.value = res.data.data }) } // 获取部门树 function getDeptTreeFun() { getDeptTree().then(res => { deptTree.value = res.data.data }) } onMounted(() => { getList() getDictList() getDeptTreeFun() }) </script> <style scoped lang="scss"> </style> <style scoped lang="scss"></style> applications/task-work-order/src/views/orderView/orderManage/orderManage/orderManageApi.js
New file @@ -0,0 +1,37 @@ import request from '@/axios' // 获取工单列表 export const gdWorkOrderPageApi = params => { return request({ url: `/drone-gd/workOrder/gdWorkOrder/page`, method: 'get', params: { descs: 'create_time', ...params }, }) } // 新增工单 export const gdWorkOrderSaveApi = data => { return request({ url: `/drone-gd/workOrder/gdWorkOrder/save`, method: 'post', data, }) } // 删除工单 export const gdWorkOrderRemoveApi = params => { return request({ url: `/drone-gd/workOrder/gdWorkOrder/remove`, method: 'post', params, }) } // 获取工单详情 export const gdWorkOrderDetailApi = params => { return request({ url: `/drone-gd/workOrder/gdWorkOrder/detail`, method: 'get', params, }) } uniapps/work-wx/src/subPackages/deviceRegistration/add.vue
@@ -597,9 +597,11 @@ onMounted(async () => { getProTypeApi() getManufacturerInfoApi() // 动态导入xzqhData,确保在使用前已初始化 const { xzqhData } = await import('@/static/xzqh/index') areaData.value = xzqhData // 动态从外部URL导入xzqhData,确保在使用前已初始化 // 注意:直接动态导入外部URL可能会有跨域或构建问题,建议检查服务器CORS设置 const response = await fetch('https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/xzqh/index.json') const text = await response.text() areaData.value = JSON.parse(text) }) </script> <style scoped lang="scss">