吉安感知网项目-前端
8 files modified
364 ■■■■■ changed files
applications/mobile-web-view/src/appComponents/workMap/index.vue 111 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/work/workDetail/index.vue 106 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/clueEvents/DistributeDiaLog.vue 8 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/clueEvents/ViewDiaLog.vue 63 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue 8 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/manifest.json 13 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/work/index.vue 51 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/workDetail/index.vue 4 ●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appComponents/workMap/index.vue
@@ -5,12 +5,25 @@
                    <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'
@@ -51,6 +64,21 @@
    })
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])
@@ -175,6 +203,42 @@
    }
}
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({
@@ -248,6 +312,11 @@
// 存储事件点标记实例,用于后续更新或移除
let incidentMarker = null
function formatCoordinate(value) {
    const numberValue = Number(value)
    return Number.isNaN(numberValue) ? value : numberValue.toFixed(6)
}
// 添加事件点标记
function addIncidentMarker(data) {
    // 检查地图和标记层是否已初始化
@@ -284,6 +353,19 @@
    // 存储关联数据
    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({
@@ -359,7 +441,8 @@
        width: 100%;
        height: 100%;
    }
    .location-btn{
    .location-btn,
    .nav-btn{
        display: flex;
        flex-direction: column;
        justify-content: center;
@@ -380,6 +463,9 @@
    .location-btn {
        bottom: 76px;
    }
    .nav-btn {
        bottom: 28px;
    }
}
// 位置标记闪烁效果
@@ -387,6 +473,29 @@
    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);
applications/mobile-web-view/src/appPages/work/workDetail/index.vue
@@ -2,12 +2,20 @@
    <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>
    <!-- 工单内容 -->
@@ -15,6 +23,10 @@
      <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>
@@ -60,11 +72,17 @@
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(() => {
@@ -77,8 +95,9 @@
    return imageArr
})
const openPreview = () => {
    if (isVideoAttachment.value || !mediaSrc.value) return
    showImagePreview({
        images: [imgSrc.value],
        images: [mediaSrc.value],
        startPosition: 0,
    })
}
@@ -100,7 +119,63 @@
    )
}
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 {
@@ -110,13 +185,15 @@
        // 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 {
@@ -124,12 +201,23 @@
        .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%;
            }
        }
    }
applications/task-work-order/src/views/orderView/orderManage/clueEvents/DistributeDiaLog.vue
@@ -11,6 +11,11 @@
        <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"
@@ -58,6 +63,7 @@
// 初始化表单数据
const initForm = () => ({
    eventName: '', // 事件名称
    disposeDept: '', // 处置部门
    disposeUser: '', // 工单处置人
})
@@ -76,6 +82,7 @@
// 校验规则
const rules = {
    eventName: fieldRules(true, 50),
    disposeDept: fieldRules(true),
    disposeUser: fieldRules(true),
}
@@ -109,6 +116,7 @@
            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,
applications/task-work-order/src/views/orderView/orderManage/clueEvents/ViewDiaLog.vue
@@ -12,6 +12,11 @@
                            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="线索编号" />
@@ -54,6 +59,13 @@
            @success="getList"
        />
    </el-dialog>
    <VideoPlayDialog
        ref="videoPlayDialogRef"
        v-if="VideoShow"
        v-model="VideoShow"
        :playUrl="currentVideo.resultUrl"
    />
</template>
<script setup>
@@ -61,6 +73,8 @@
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')
@@ -75,6 +89,8 @@
const currentRow = ref(null) // 当前行数据
const distributeDialogRef = ref(null)
const distributeDialogVisible = ref(false)
const VideoShow = ref(false)
const currentVideo = ref({})
// 分发状态选项
const distributeStatusOptions = [
@@ -103,7 +119,7 @@
    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
@@ -127,6 +143,12 @@
    })
}
// 点击视频
function videoClick(row) {
    currentVideo.value = row
    VideoShow.value = true
}
// 打开弹框
async function open({ row } = {}) {
    currentRow.value = row
@@ -141,4 +163,43 @@
    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>
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -83,7 +83,9 @@
                        </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"
@@ -300,7 +302,7 @@
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'
@@ -515,7 +517,7 @@
        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 }
            })
        )
uniapps/work-app/src/manifest.json
@@ -30,7 +30,10 @@
        },
        /* 模块配置 */
        "modules" : {
            "Share" : {}
            "Share" : {},
            "Geolocation" : {},
            "VideoPlayer" : {},
            "Camera" : {}
        },
        /* 应用发布信息 */
        "distribute" : {
@@ -73,6 +76,11 @@
                        "appid" : "gh_f2fbbd1d5d2a",
                        "UniversalLinks" : ""
                    }
                },
                "geolocation" : {
                    "system" : {
                        "__platform__" : [ "android" ]
                    }
                }
            },
            "icons" : {
@@ -113,6 +121,9 @@
        "router" : {
            "mode" : "hash",
            "base" : "/work-app/"
        },
        "sdkConfigs" : {
            "maps" : {}
        }
    },
    "locale" : "zh-Hans",
uniapps/work-app/src/pages/work/index.vue
@@ -13,12 +13,17 @@
        <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>
@@ -383,13 +388,40 @@
      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;
@@ -398,14 +430,16 @@
        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;
@@ -416,7 +450,8 @@
          font-weight: 400;
          font-size: 24rpx;
          color: #FFFFFF;
          padding-right: 14rpx;
          margin-left: 12rpx;
                    white-space: nowrap;
        }
      }
uniapps/work-app/src/subPackages/workDetail/index.vue
@@ -30,6 +30,10 @@
        <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>