| | |
| | | <van-image width="20" height="20" :src="locationIcon" /> |
| | | <div class="label">定位</div> |
| | | </div> |
| | | <div class="nav-btn" @click="openNavigation" v-show="navigationTarget"> |
| | | <van-image width="20" height="20" :src="mapnavIcon" /> |
| | | <div class="label">导航</div> |
| | | </div> |
| | | <van-action-sheet |
| | | v-model:show="navigationSheetShow" |
| | | :actions="navigationActions" |
| | | cancel-text="取消" |
| | | @select="handleNavigationSelect" |
| | | /> |
| | | |
| | | |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { searchGeocoder } from '@/utils/util' |
| | | import { showToast } from 'vant' |
| | | import mapnavIcon from '@/appDataSource/appwork/mapnav.svg' |
| | | import incidentPoint from '@/appDataSource/appwork/positioning1.svg' |
| | | import userLocationIcon from '@/appDataSource/leafletMapIcon/user-location.svg' |
| | |
| | | }) |
| | | const currentAddress = ref('') |
| | | const AMAP_KEY = '5b8ba312d053e4bdb44911733ece7d63' |
| | | const navigationSheetShow = ref(false) |
| | | const navigationActions = [{ name: '高德地图' }, { name: '百度地图' }, { name: '腾讯地图' }] |
| | | const navigationTarget = computed(() => { |
| | | if (!workNavigationShow || !mapCurrentDetail?.longitude || !mapCurrentDetail?.latitude) return null |
| | | |
| | | const lat = parseFloat(mapCurrentDetail.latitude) |
| | | const lng = parseFloat(mapCurrentDetail.longitude) |
| | | if (Number.isNaN(lat) || Number.isNaN(lng)) return null |
| | | |
| | | return { |
| | | lat, |
| | | lng, |
| | | name: mapCurrentDetail.eventLocation || mapCurrentDetail.address || '工单位置', |
| | | } |
| | | }) |
| | | |
| | | const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1]) |
| | | const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3]) |
| | |
| | | } |
| | | } |
| | | |
| | | function openNavigation() { |
| | | if (!navigationTarget.value) { |
| | | showToast('暂无可导航的位置') |
| | | return |
| | | } |
| | | |
| | | navigationSheetShow.value = true |
| | | } |
| | | |
| | | function handleNavigationSelect(action) { |
| | | navigationSheetShow.value = false |
| | | const url = getNavigationUrl(action.name, navigationTarget.value) |
| | | if (url) { |
| | | window.location.href = url |
| | | } |
| | | } |
| | | |
| | | function getNavigationUrl(type, target) { |
| | | const [gcjLng, gcjLat] = wgs84ToGcj02(target.lng, target.lat) |
| | | const name = encodeURIComponent(target.name) |
| | | |
| | | if (type === '高德地图') { |
| | | return `https://uri.amap.com/navigation?to=${gcjLng},${gcjLat},${name}&mode=car&policy=1&coordinate=gaode&callnative=1` |
| | | } |
| | | |
| | | if (type === '百度地图') { |
| | | return `https://api.map.baidu.com/direction?destination=latlng:${gcjLat},${gcjLng}|name:${name}&mode=driving&coord_type=gcj02&output=html&src=ja-web` |
| | | } |
| | | |
| | | if (type === '腾讯地图') { |
| | | return `https://apis.map.qq.com/uri/v1/routeplan?type=drive&tocoord=${gcjLat},${gcjLng}&to=${name}&referer=ja-web` |
| | | } |
| | | |
| | | return '' |
| | | } |
| | | |
| | | let lastLocationMarker = null |
| | | const getMapLocation = async () => { |
| | | const customIcon = L.icon({ |
| | |
| | | // 存储事件点标记实例,用于后续更新或移除 |
| | | let incidentMarker = null |
| | | |
| | | function formatCoordinate(value) { |
| | | const numberValue = Number(value) |
| | | return Number.isNaN(numberValue) ? value : numberValue.toFixed(6) |
| | | } |
| | | |
| | | // 添加事件点标记 |
| | | function addIncidentMarker(data) { |
| | | // 检查地图和标记层是否已初始化 |
| | |
| | | |
| | | // 存储关联数据 |
| | | incidentMarker.options.customData = data |
| | | incidentMarker.bindTooltip( |
| | | `<div class="coordinate-label"> |
| | | <div>经度:${formatCoordinate(lng)}</div> |
| | | <div>纬度:${formatCoordinate(lat)}</div> |
| | | </div>`, |
| | | { |
| | | permanent: true, |
| | | direction: 'top', |
| | | offset: L.point(0, -16), |
| | | opacity: 1, |
| | | className: 'incident-coordinate-tooltip', |
| | | } |
| | | ) |
| | | |
| | | // 定位到该标记点 |
| | | mapSetView({ |
| | |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .location-btn{ |
| | | .location-btn, |
| | | .nav-btn{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | |
| | | .location-btn { |
| | | bottom: 76px; |
| | | } |
| | | .nav-btn { |
| | | bottom: 28px; |
| | | } |
| | | } |
| | | |
| | | // 位置标记闪烁效果 |
| | |
| | | animation: blink 1s ease-in-out; |
| | | } |
| | | |
| | | :deep(.incident-coordinate-tooltip) { |
| | | padding: 0; |
| | | border: 0; |
| | | background: transparent; |
| | | box-shadow: none; |
| | | |
| | | &::before { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | :deep(.coordinate-label) { |
| | | padding: 4px 7px; |
| | | background: rgba(255, 255, 255, 0.95); |
| | | border: 1px solid rgba(76, 133, 255, 0.35); |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); |
| | | font-size: 11px; |
| | | line-height: 16px; |
| | | color: #222324; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | @keyframes blink { |
| | | 0% { |
| | | transform: scale(1); |
| | |
| | | <div class="workDetailContainer"> |
| | | <div class="detailTop"> |
| | | <div class="image-container"> |
| | | <van-swipe :autoplay="3000" indicator-color="#4C85FF"> |
| | | <van-swipe-item v-for="(img, index) in [imgSrc]" :key="index"> |
| | | <van-image class="detailImage" :src="img" fit="cover" width="100%" height="235px" |
| | | @click="openPreview(index)" preview-visible="false" /> |
| | | </van-swipe-item> |
| | | </van-swipe> |
| | | <video |
| | | v-if="isVideoAttachment && mediaSrc" |
| | | ref="videoRef" |
| | | class="video-js vjs-default-skin vjs-big-play-centered detailVideo" |
| | | controls |
| | | preload="auto" |
| | | playsinline |
| | | ></video> |
| | | <van-swipe v-else :autoplay="3000" indicator-color="#4C85FF"> |
| | | <van-swipe-item v-for="(img, index) in [mediaSrc]" :key="index"> |
| | | <van-image class="detailImage" :src="img" fit="cover" width="100%" height="235px" |
| | | @click="openPreview(index)" preview-visible="false" /> |
| | | </van-swipe-item> |
| | | </van-swipe> |
| | | </div> |
| | | </div> |
| | | <!-- 工单内容 --> |
| | |
| | | <div class="workOrderContent"> |
| | | <div class="workOrderTitle">工单内容</div> |
| | | <div class="workOrderContainer"> |
| | | <div class="orderRow"> |
| | | <div class="rowTitle">工单名称</div> |
| | | <div>{{ workDetailData.eventName }}</div> |
| | | </div> |
| | | <div class="orderRow"> |
| | | <div class="rowTitle">工单编号</div> |
| | | <div>{{ workDetailData.eventNum }}</div> |
| | |
| | | import { getShowImg, getSmallImg } from '@/utils/util' |
| | | import { useRoute,useRouter } from 'vue-router' |
| | | import { getAiImg } from '@ztzf/utils' |
| | | import videojs from 'video.js' |
| | | import 'video.js/dist/video-js.css' |
| | | const keyword = ref('') |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | |
| | | const workDetailData = ref({}) |
| | | const videoRef = ref(null) |
| | | let player = null |
| | | |
| | | const isVideoAttachment = computed(() => Number(workDetailData.value.attachmentType) === 3) |
| | | |
| | | // 预览图片 |
| | | const getImageList = computed(() => { |
| | |
| | | return imageArr |
| | | }) |
| | | const openPreview = () => { |
| | | if (isVideoAttachment.value || !mediaSrc.value) return |
| | | showImagePreview({ |
| | | images: [imgSrc.value], |
| | | images: [mediaSrc.value], |
| | | startPosition: 0, |
| | | }) |
| | | } |
| | |
| | | ) |
| | | } |
| | | |
| | | const imgSrc = ref('') |
| | | function getVideoType(url) { |
| | | const path = (url || '').split('?')[0].toLowerCase() |
| | | if (path.endsWith('.m3u8')) return 'application/x-mpegURL' |
| | | if (path.endsWith('.mp4')) return 'video/mp4' |
| | | if (path.endsWith('.webm')) return 'video/webm' |
| | | if (path.endsWith('.ogg') || path.endsWith('.ogv')) return 'video/ogg' |
| | | if (path.endsWith('.mov')) return 'video/quicktime' |
| | | return '' |
| | | } |
| | | |
| | | function getVideoSource() { |
| | | const type = getVideoType(mediaSrc.value) |
| | | return { |
| | | src: mediaSrc.value, |
| | | ...(type ? { type } : {}), |
| | | } |
| | | } |
| | | |
| | | function destroyPlayer() { |
| | | if (!player) return |
| | | player.dispose() |
| | | player = null |
| | | } |
| | | |
| | | async function initPlayer() { |
| | | if (!isVideoAttachment.value || !mediaSrc.value) return |
| | | await nextTick() |
| | | if (!videoRef.value) return |
| | | |
| | | if (player) { |
| | | player.src(getVideoSource()) |
| | | player.load() |
| | | return |
| | | } |
| | | |
| | | player = videojs(videoRef.value, { |
| | | controls: true, |
| | | preload: 'auto', |
| | | autoplay: false, |
| | | fluid: false, |
| | | sources: [getVideoSource()], |
| | | }) |
| | | player.on('error', () => { |
| | | console.error('视频播放失败:', player?.error(), mediaSrc.value) |
| | | }) |
| | | } |
| | | |
| | | const mediaSrc = ref('') |
| | | |
| | | watch([isVideoAttachment, mediaSrc], () => { |
| | | if (isVideoAttachment.value) { |
| | | initPlayer() |
| | | } else { |
| | | destroyPlayer() |
| | | } |
| | | }) |
| | | |
| | | onMounted(async () => { |
| | | keyword.value = JSON.parse(route.query.workDetailData) |
| | | try { |
| | |
| | | // http://220.177.172.27:8100 改为 https://wrj.shuixiongit.com/ja-proxy |
| | | eventImageUrl = replaceWithProxy(eventImageUrl) |
| | | if (eventImageUrl){ |
| | | imgSrc.value = geojson ? await getAiImg(eventImageUrl,geojson) : eventImageUrl |
| | | mediaSrc.value = !isVideoAttachment.value && geojson ? await getAiImg(eventImageUrl,geojson) : eventImageUrl |
| | | } |
| | | } catch (error) { |
| | | showToast('分享链接失效') |
| | | } |
| | | |
| | | }) |
| | | |
| | | onBeforeUnmount(destroyPlayer) |
| | | </script> |
| | | <style lang="scss" scoped> |
| | | .workDetailContainer { |
| | |
| | | .image-container { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 235px; |
| | | .detailImage { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: block; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .detailVideo { |
| | | width: 100%; |
| | | height: 235px; |
| | | } |
| | | |
| | | :deep(.video-js .vjs-tech) { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | <el-form class="gd-dialog-form" ref="formRef" :model="formData" :rules="rules" label-width="100px"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="事件名称" prop="eventName"> |
| | | <el-input class="gd-input" v-model="formData.eventName" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="处置部门" prop="disposeDept"> |
| | | <el-tree-select |
| | | class="gd-select" |
| | |
| | | |
| | | // 初始化表单数据 |
| | | const initForm = () => ({ |
| | | eventName: '', // 事件名称 |
| | | disposeDept: '', // 处置部门 |
| | | disposeUser: '', // 工单处置人 |
| | | }) |
| | |
| | | |
| | | // 校验规则 |
| | | const rules = { |
| | | eventName: fieldRules(true, 50), |
| | | disposeDept: fieldRules(true), |
| | | disposeUser: fieldRules(true), |
| | | } |
| | |
| | | areaCode: currentRow.value.areaCode || '', |
| | | disposeDept: formData.value.disposeDept, |
| | | disposeUser: formData.value.disposeUser, |
| | | eventName: formData.value.eventName, |
| | | latitude: currentRow.value.latitude || 0, |
| | | longitude: currentRow.value.longitude || 0, |
| | | resultId: currentRow.value.id, |
| | |
| | | style="width: 80px; height: 80px" |
| | | preview-teleported |
| | | /> |
| | | <div class="video-btn" v-if="row.attachmentType === 3" @click="videoClick(row)"> |
| | | <el-icon :size="30" color="#fff"> |
| | | <VideoPlay /> |
| | | </el-icon> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="resultCode" show-overflow-tooltip label="线索编号" /> |
| | |
| | | @success="getList" |
| | | /> |
| | | </el-dialog> |
| | | |
| | | <VideoPlayDialog |
| | | ref="videoPlayDialogRef" |
| | | v-if="VideoShow" |
| | | v-model="VideoShow" |
| | | :playUrl="currentVideo.resultUrl" |
| | | /> |
| | | </template> |
| | | |
| | | <script setup> |
| | |
| | | import { gdTaskResultListApi } from './achievementApi' |
| | | import DistributeDiaLog from './DistributeDiaLog.vue' |
| | | import { getAiImg } from '@ztzf/utils' |
| | | import { VideoPlay } from '@element-plus/icons-vue' |
| | | import VideoPlayDialog from '@/components/VideoPlayDialog.vue' |
| | | |
| | | const store = useStore() |
| | | const requester = computed(() => store.state.user.userInfo?.role_id === '2014158512610869250') |
| | |
| | | const currentRow = ref(null) // 当前行数据 |
| | | const distributeDialogRef = ref(null) |
| | | const distributeDialogVisible = ref(false) |
| | | const VideoShow = ref(false) |
| | | const currentVideo = ref({}) |
| | | |
| | | // 分发状态选项 |
| | | const distributeStatusOptions = [ |
| | |
| | | if (!currentRow.value?.id) return |
| | | loading.value = true |
| | | try { |
| | | const res = await gdTaskResultListApi({ patrolTaskId: currentRow.value.id,attachmentType: '1,2' }) |
| | | const res = await gdTaskResultListApi({ patrolTaskId: currentRow.value.id}) |
| | | list.value = await Promise.all( |
| | | (res?.data?.data ?? []).map(async item => { |
| | | if (item.attachmentType !== 2) return item |
| | |
| | | }) |
| | | } |
| | | |
| | | // 点击视频 |
| | | function videoClick(row) { |
| | | currentVideo.value = row |
| | | VideoShow.value = true |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ row } = {}) { |
| | | currentRow.value = row |
| | |
| | | color: #c0c4cc; |
| | | cursor: not-allowed; |
| | | } |
| | | |
| | | .video-btn { |
| | | width: 80px; |
| | | height: 80px; |
| | | position: relative; |
| | | overflow: hidden; |
| | | border-radius: 4px; |
| | | background: linear-gradient(135deg, rgba(76, 52, 255, 0.14), rgba(76, 52, 255, 0) 48%), |
| | | linear-gradient(180deg, #f4f5ff 0%, #e9ecff 100%); |
| | | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | |
| | | &::before { |
| | | content: ''; |
| | | position: absolute; |
| | | inset: 0; |
| | | background-image: linear-gradient( |
| | | 90deg, |
| | | rgba(76, 52, 255, 0.05) 0, |
| | | rgba(76, 52, 255, 0.05) 1px, |
| | | transparent 1px, |
| | | transparent 12px |
| | | ); |
| | | opacity: 0.42; |
| | | } |
| | | |
| | | .el-icon { |
| | | position: relative; |
| | | z-index: 1; |
| | | width: 42px; |
| | | height: 42px; |
| | | border-radius: 50%; |
| | | background: rgba(76, 52, 255, 0.72); |
| | | box-shadow: 0 4px 12px rgba(76, 52, 255, 0.2); |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | <div class="imgBox"> |
| | | <div |
| | | v-for="item in taskResultList.filter(item1 => item1.resultUrl && [1, 2, 3].includes(item1.attachmentType))" |
| | | v-for="item in taskResultList.filter( |
| | | item1 => item1.resultUrl && [1, 2, 3].includes(item1.attachmentType) |
| | | )" |
| | | > |
| | | <el-image |
| | | v-if="item.attachmentType === 1 || item.attachmentType === 2" |
| | |
| | | import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue' |
| | | import { gdTaskResultListApi } from '@/views/orderView/orderManage/clueEvents/achievementApi' |
| | | import RefuseOrderDialog1 from '@/views/orderView/orderManage/inspectionRequest/RefuseOrderDialog1.vue' |
| | | import { Check } from '@element-plus/icons-vue' |
| | | import { Check, VideoPlay } from '@element-plus/icons-vue' |
| | | import { queryAirById, airlineListApi, algorithmGroupedApi } from '@/api/zkxt' |
| | | import * as Cesium from 'cesium' |
| | | import { useStore } from 'vuex' |
| | |
| | | taskResultList.value = await Promise.all( |
| | | (res?.data?.data ?? []).map(async item => { |
| | | if (item.attachmentType !== 2) return item |
| | | const aiImg = await getAiImg(item.resultUrl,item.geojson) |
| | | const aiImg = await getAiImg(item.resultUrl, item.geojson) |
| | | return { ...item, aiImg } |
| | | }) |
| | | ) |
| | |
| | | }, |
| | | /* 模块配置 */ |
| | | "modules" : { |
| | | "Share" : {} |
| | | "Share" : {}, |
| | | "Geolocation" : {}, |
| | | "VideoPlayer" : {}, |
| | | "Camera" : {} |
| | | }, |
| | | /* 应用发布信息 */ |
| | | "distribute" : { |
| | |
| | | "appid" : "gh_f2fbbd1d5d2a", |
| | | "UniversalLinks" : "" |
| | | } |
| | | }, |
| | | "geolocation" : { |
| | | "system" : { |
| | | "__platform__" : [ "android" ] |
| | | } |
| | | } |
| | | }, |
| | | "icons" : { |
| | |
| | | "router" : { |
| | | "mode" : "hash", |
| | | "base" : "/work-app/" |
| | | }, |
| | | "sdkConfigs" : { |
| | | "maps" : {} |
| | | } |
| | | }, |
| | | "locale" : "zh-Hans", |
| | |
| | | <div class="eventList"> |
| | | <div class="eventItem" v-for="(item,index) in dataList" :key="index"> |
| | | <image |
| | | v-if="[1, 2].includes(item.attachmentType)" |
| | | :src="item?.aiImg || item?.eventImageUrl" |
| | | mode="aspectFill" |
| | | @click="detailHandle(item)" |
| | | /> |
| | | <div v-if="item.attachmentType === 3" class="videoBox" @click="detailHandle(item)"> |
| | | <div class="playIcon"></div> |
| | | </div> |
| | | <div class="informationDisplay"> |
| | | <div class="itemContent">{{formatDate(item.createTime) }}</div> |
| | | <div class="itemTitle">{{ item.eventName }}</div> |
| | | <div class="itemContent">{{ formatDate(item.createTime) }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | box-sizing: border-box; |
| | | max-width: 100%; |
| | | |
| | | image { |
| | | image, |
| | | .videoBox { |
| | | width: 100%; |
| | | height: 208rpx; |
| | | border-radius: 12rpx; |
| | | overflow: hidden; |
| | | display: block; |
| | | } |
| | | .videoBox { |
| | | background: linear-gradient(135deg, #eef4ff 0%, #dfe8f8 100%); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .playIcon { |
| | | width: 72rpx; |
| | | height: 72rpx; |
| | | border-radius: 50%; |
| | | background: rgba(29, 111, 233, 0.9); |
| | | position: relative; |
| | | box-shadow: 0 8rpx 18rpx rgba(29, 111, 233, 0.24); |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 29rpx; |
| | | top: 20rpx; |
| | | width: 0; |
| | | height: 0; |
| | | border-top: 16rpx solid transparent; |
| | | border-bottom: 16rpx solid transparent; |
| | | border-left: 22rpx solid #fff; |
| | | } |
| | | } |
| | | .informationDisplay{ |
| | | width: 100%; |
| | | position: absolute; |
| | |
| | | border-radius: 0rpx 0rpx 12rpx 12rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | padding: 10rpx 0rpx 10rpx 0rpx; |
| | | justify-content: space-between; |
| | | padding: 10rpx 14rpx; |
| | | box-sizing: border-box; |
| | | .itemTitle { |
| | | width: 144rpx; |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 500; |
| | | font-size: 28rpx; |
| | | color: #000000; |
| | | font-size: 24rpx; |
| | | color: #FFFFFF; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | |
| | | font-weight: 400; |
| | | font-size: 24rpx; |
| | | color: #FFFFFF; |
| | | padding-right: 14rpx; |
| | | margin-left: 12rpx; |
| | | white-space: nowrap; |
| | | |
| | | } |
| | | } |
| | |
| | | <div class="workOrderTitle">工单内容</div> |
| | | <div class="workOrderContainer"> |
| | | <div class="orderRow"> |
| | | <div class="rowTitle">工单名称</div> |
| | | <div>{{ workDetailData.eventName }}</div> |
| | | </div> |
| | | <div class="orderRow"> |
| | | <div class="rowTitle">工单编号</div> |
| | | <div>{{ workDetailData.eventNum }}</div> |
| | | </div> |