吉安感知网项目-前端
shuishen
2026-01-29 0ef47a49fa6dce546be2d0f23b4949734e6a0cf9
Merge branch 'master' of http://139.196.74.78:10010/r/jagzwxm/ja_web
24 files modified
3 files added
2500 ■■■■ changed files
applications/drone-command/src/assets/images/areaManage/positionIcon.png patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue 271 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/audioCall.vue 600 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/DeviceScrapDiaLog.vue 25 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/fwDeviceScrapApi.js 11 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/index.vue 30 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/maintainRecord/FormDiaLog.vue 63 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/equipmentResource/FormDiaLog.vue 631 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/equipmentResource/index.vue 237 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/flyingHand/FlyingHandDialog.vue 4 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/flyingHand/index.vue 6 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue 82 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue 141 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionReport/index.vue 85 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionReport/inspectionRequestApi.js 9 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/FormDiaLog.vue 1 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue 31 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue 5 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/organizational/agenciesManagement/agenciesApi.js 9 ●●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/organizational/agenciesManagement/index.vue 61 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/organizational/zoningManagement/index.vue 61 ●●●● patch | view | raw | blame | history
applications/task-work-order/src/views/orderView/organizational/zoningManagement/zoningApi.js 9 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/api/index.js 8 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/page/index.vue 16 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/store/modules/app/index.js 7 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/flightApplication/details.vue 91 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/flightApplication/index.vue 6 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/areaManage/positionIcon.png
applications/drone-command/src/views/areaManage/precinctInfo/FormDiaLog.vue
@@ -1,6 +1,10 @@
<template>
    <el-dialog class="command-page-view-dialog" v-model="visible" :title="titleEnum[dialogMode]"
        :close-on-click-modal="false">
    <el-dialog
        class="command-page-view-dialog"
        v-model="visible"
        :title="titleEnum[dialogMode]"
        :close-on-click-modal="false"
    >
        <div class="detail-container" v-if="readonly">
            <el-row>
                <el-col :span="12">
@@ -12,75 +16,98 @@
                    <div class="val">{{ formData.contactPerson }}</div>
                </el-col>
                <el-col :span="12">
                    <div class="label">位置</div>
                    <div class="val">{{ formData.address }}</div>
                </el-col>
                <el-col :span="12">
                    <div class="label">联系方式</div>
                    <div class="val">{{ formData.contactPhone }}</div>
                </el-col>
                <el-col :span="12">
                    <div class="label">位置</div>
                    <div class="val">{{ formData.address }}</div>
                </el-col>
            </el-row>
        </div>
        <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
            label-width="100px">
        <el-form
            class="dialog-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="stationName">
                        <el-input class="command-input" v-model="formData.stationName" maxlength="50"
                            placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <el-col :span="24">
                    <el-form-item label="位置" prop="longitude">
                        <el-button  class="address-btn" @click="selectLocation">
                            {{ formData.longitude }}, {{ formData.latitude }}, {{ formData.address }}
                        </el-button>
                        <el-input
                            class="command-input"
                            v-model="formData.stationName"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="联系人" prop="contactPerson">
                        <el-input class="command-input" v-model="formData.contactPerson" maxlength="50"
                            placeholder="请输入" clearable />
                        <el-input
                            class="command-input"
                            v-model="formData.contactPerson"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="联系方式" prop="contactPhone">
                        <el-input class="command-input" v-model="formData.contactPhone" maxlength="50"
                            placeholder="请输入" clearable />
                        <el-input
                            class="command-input"
                            v-model="formData.contactPhone"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="24">
                    <el-form-item label="位置" prop="longitude">
                        <div class="location-container">
                            <div class="location-info" v-if="formData.longitude && formData.latitude">
                                经度: {{ formData.longitude }}, 纬度: {{ formData.latitude }}
                                <span v-if="formData.address">, {{ formData.address }}</span>
                            </div>
                            <div class="location-info" v-else>请在地图上点击选择位置</div>
                            <div class="inline-map-container">
                                <CommonCesiumMap
                                    ref="inlineMapRef"
                                    class="inline-map command-cesium"
                                    :active="visible && !readonly"
                                    :flat-mode="false"
                                    :terrain="true"
                                    :layer-mode="4"
                                    :boundary="false"
                                />
                            </div>
                        </div>
                    </el-form-item>
                </el-col>
            </el-row>
        </el-form>
        <template #footer>
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
            <el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting" :disabled="submitting"
                @click="handleSubmit">
            <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">
                {{ readonly ? '关闭' : '取消' }}
            </el-button>
            <el-button
                color="#284FE3"
                v-if="!readonly"
                type="primary"
                :loading="submitting"
                :disabled="submitting"
                @click="handleSubmit"
            >
                确定
            </el-button>
        </template>
    </el-dialog>
    <el-dialog class="command-page-map-view-dialog" v-model="visible1" :show-close="false"
            :close-on-click-modal="false" destroy-on-close @closed="positionClosed">
            <div class="dialog-container">
                <div class="left-container">
                    <CommonCesiumMap
                        ref="mapRef"
                        class="leftMap command-cesium"
                        :active="visible1"
                        :flat-mode="false"
                        :terrain="true"
                        :layer-mode="4"
                        :boundary="false"
                    />
                </div>
            </div>
            <template #footer>
                <el-button color="#2B2B4C" @click="handleMapCancel">取消</el-button>
                <el-button color="#284FE3" type="primary" @click="handleMapConfirm">确定</el-button>
            </template>
        </el-dialog>
</template>
<script setup>
@@ -93,27 +120,26 @@
import axios from 'axios'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
import positionIcon from '@/assets/images/areaManage/positionIcon.png'
const initForm = () => ({
    address: '', // 位置
    contactPerson: '', // 联系人
    contactPhone: '', // 联系方式
    stationName: '', // 派出所名称
    longitude: 0,
    latitude: 0,
    longitude: null,
    latitude: null,
})
const emit = defineEmits(['success'])
const formRef = ref(null) // 表单实例
const formData = ref(initForm()) // 表单数据
const visible = defineModel() // 弹框显隐
const visible1 = ref(false) // 弹框显隐
const dialogMode = ref('add') // 弹框模式
const submitting = ref(false) // 提交中
const readonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const tempLocation = ref({ longitude: null, latitude: null })
const mapRef = ref(null)
const inlineMapRef = ref(null) // 内嵌地图引用
const route = useRoute()
const rules = {
@@ -121,15 +147,16 @@
    stationName: fieldRules(true, 50),
    contactPerson: fieldRules(true, 50),
    contactPhone: fieldRules(true, 50),
    longitude: fieldRules(true),
}
// 关闭弹框
function handleCancel () {
function handleCancel() {
    visible.value = false
}
// 提交新增/编辑
async function handleSubmit () {
async function handleSubmit() {
    const isValid = await formRef.value?.validate().catch(() => false)
    if (!isValid) return
    submitting.value = true
@@ -149,13 +176,31 @@
}
// 加载详情
async function loadDetail () {
async function loadDetail() {
    if (!formData.value.id) return
    const res = await fwPoliceStationDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? {}
}
function LeftClickEvent (click) {
let viewer
let redPointEntity
let leftClickBound = false
const labelParams = {
    font: '18px',
    fillColor: Cesium.Color.WHITE, // 文字颜色:白色
    backgroundColor: Cesium.Color.BLACK, //背景颜色
    backgroundPadding: new Cesium.Cartesian2(5, 5), // 水平/垂直内边距(像素)
    pixelOffset: new Cesium.Cartesian2(0, -20), // 可选:微调位置
    showBackground: true, // 确保背景显示(某些版本需要)
    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
    horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
    verticalOrigin: Cesium.VerticalOrigin.CENTER,
    outlineWidth: 1,
    outlineColor: Cesium.Color.WHITE,
    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
}
function LeftClickEvent(click) {
    const pos = click.position // Cartesian2 屏幕坐标
    // 屏幕坐标 -> 椭球面坐标(不考虑地形/模型)
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
@@ -166,29 +211,24 @@
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Cesium.Math.toDegrees(carto.longitude)
    const latitude = Cesium.Math.toDegrees(carto.latitude)
    tempLocation.value = { longitude, latitude }
    // 直接更新 formData
    formData.value.longitude = _.round(longitude, 6)
    formData.value.latitude = _.round(latitude, 6)
    getLocationName(longitude, latitude)
    // 添加红点(全局仅保留一个)并渲染经纬度标签
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            billboard: {
                image: positionIcon,
                width: 20,
                height: 20,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE, // 文字颜色:白色
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.6), //背景颜色
                backgroundPadding: new Cesium.Cartesian2(5, 5), // 水平/垂直内边距(像素)
                pixelOffset: new Cesium.Cartesian2(0, -20), // 可选:微调位置
                showBackground: true, // 确保背景显示(某些版本需要)
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 负值更靠近相机(显示在前)
                ...labelParams
            },
        })
    } else {
@@ -197,36 +237,20 @@
    }
}
function positionClosed () {
    redPointEntity = null
}
let viewer
let redPointEntity
let leftClickBound = false
function setMapPoint (longitude, latitude) {
function setMapPoint(longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            billboard: {
                image: positionIcon,
                width: 20,
                height: 20,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.6),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                pixelOffset: new Cesium.Cartesian2(0, -20),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20),
                ...labelParams
            },
        })
    } else {
@@ -235,34 +259,27 @@
    }
}
function formatCoord (value) {
function formatCoord(value) {
    if (value == null || value === '') return '-'
    return _.round(Number(value), 6)
}
async function selectLocation () {
    visible1.value = true
// 初始化内嵌地图
async function initInlineMap() {
    await nextTick()
    const map = mapRef.value?.getMap()
    const map = inlineMapRef.value?.getMap()
    viewer = map?.viewer || null
    if (viewer && !leftClickBound) {
        map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
        leftClickBound = true
    }
    if (formData.value.longitude != null && formData.value.latitude != null) {
        tempLocation.value = { longitude: formData.value.longitude, latitude: formData.value.latitude }
        setMapPoint(formData.value.longitude, formData.value.latitude)
    }
}
function handleMapCancel () {
    tempLocation.value = { longitude: null, latitude: null }
    visible1.value = false
}
async function getLocationName () {
async function getLocationName(longitude, latitude) {
    const tk = import.meta.env.VITE_APP_TDT_TOKEN
    const { latitude, longitude } = tempLocation.value
    const http = axios.create({ withCredentials: false })
    const res = await http({
        url: `https://api.tianditu.gov.cn/geocoder?postStr={'lon':${longitude},'lat':${latitude},'ver':1}&tk=${tk}`,
@@ -271,24 +288,44 @@
    formData.value.address = res.data.result.formatted_address
}
function handleMapConfirm () {
    if (tempLocation.value.longitude != null && tempLocation.value.latitude != null) {
        formData.value.longitude = _.round(tempLocation.value.longitude, 6)
        formData.value.latitude = _.round(tempLocation.value.latitude, 6)
        getLocationName()
    }
    visible1.value = false
}
// 打开弹框
async function open ({ mode, row } = {}) {
async function open({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    dialogMode.value !== 'add' && (await loadDetail())
    redPointEntity = null
    leftClickBound = false
    if (dialogMode.value !== 'add') {
        await loadDetail()
    }
    // 延迟初始化地图,确保 DOM 渲染完成
    setTimeout(() => {
        initInlineMap()
    }, 500)
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.location-container {
    width: 100%;
}
.location-info {
    margin-bottom: 8px;
    color: #fff;
    font-size: 14px;
}
.inline-map-container {
    width: 100%;
    height: 300px;
    border-radius: 4px;
    overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.1);
}
.inline-map {
    width: 100%;
    height: 100%;
}
</style>
applications/drone-command/src/views/audioCall.vue
@@ -1,439 +1,439 @@
<template>
    <div style="padding:16px">
      <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
      <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
        <span>我的 userId:</span>
        <input v-model="uid" style="width:120px" />
        <span>对方 userId:</span>
        <input v-model="peerUid" style="width:120px" />
        <button @click="connectWS" :disabled="connected">连接WS</button>
        <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
        <button v-if="state==='ringing'" @click="acceptCall">接听</button>
        <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
        <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
        <strong>状态:{{ state }}</strong>
        <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
      </div>
      <div style="margin-top:8px;color:#666">
        <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
      </div>
      <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
      <audio ref="remoteAudio" autoplay></audio>
        <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
            <span>我的 userId:</span>
            <input v-model="uid" style="width:120px" />
            <span>对方 userId:</span>
            <input v-model="peerUid" style="width:120px" />
            <button @click="connectWS" :disabled="connected">连接WS</button>
            <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
            <button v-if="state==='ringing'" @click="acceptCall">接听</button>
            <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
            <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
            <strong>状态:{{ state }}</strong>
            <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
        </div>
        <div style="margin-top:8px;color:#666">
            <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
        </div>
        <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
        <audio ref="remoteAudio" autoplay></audio>
    </div>
  </template>
  <script setup>
  import { ref, computed, onBeforeUnmount } from 'vue'
  /** ✅ 你的 WS 地址前缀(后面拼 userId) */
  const WS_BASE = 'wss://域名/ws/chat?userId='
  const uid = ref('2')
  const peerUid = ref('3')
  const connected = ref(false)
  const state = ref('idle') // idle | calling | ringing | in-call
  const logText = ref('')
  const remoteAudio = ref(null)
  let ws = null
  let pc = null
  let localStream = null
  const pendingCandidates = []
  // 来电暂存
  let incomingFrom = null
  let acceptedByMe = false // 我是否已经点了接听
  let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
  // 计时
  let timer = null
  const seconds = ref(0)
  const durationText = computed(() => {
</template>
<script setup>
import { ref, computed, onBeforeUnmount } from 'vue'
/** ✅ 你的 WS 地址前缀(后面拼 userId) */
const WS_BASE = 'wss://域名/ws/chat?userId='
const uid = ref('2')
const peerUid = ref('3')
const connected = ref(false)
const state = ref('idle') // idle | calling | ringing | in-call
const logText = ref('')
const remoteAudio = ref(null)
let ws = null
let pc = null
let localStream = null
const pendingCandidates = []
// 来电暂存
let incomingFrom = null
let acceptedByMe = false // 我是否已经点了接听
let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
// 计时
let timer = null
const seconds = ref(0)
const durationText = computed(() => {
    const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
    const s = String(seconds.value % 60).padStart(2, '0')
    return `${m}:${s}`
  })
  function startTimer() {
})
function startTimer() {
    stopTimer()
    seconds.value = 0
    timer = setInterval(() => seconds.value++, 1000)
  }
  function stopTimer() {
}
function stopTimer() {
    if (timer) clearInterval(timer)
    timer = null
  }
  // 心跳
  let pingTimer = null
  function startPing() {
}
// 心跳
let pingTimer = null
function startPing() {
    stopPing()
    // 每 25 秒发一次 ping(你后端有 PING/PONG)
    pingTimer = setInterval(() => {
      if (connected.value) send('ping', 'system', null)
        if (connected.value) send('ping', 'system', null)
    }, 25000)
  }
  function stopPing() {
}
function stopPing() {
    if (pingTimer) clearInterval(pingTimer)
    pingTimer = null
  }
  function log(...args) {
}
function log(...args) {
    const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ')
    logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n`
    console.log(...args)
  }
  /**
   * ✅ 严格按后端 ChatMessage 字段发:
   * { type, from, to, payload, timestamp }
   * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
   */
  function send(type, to, payload) {
}
/**
 * ✅ 严格按后端 ChatMessage 字段发:
 * { type, from, to, payload, timestamp }
 * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
 */
function send(type, to, payload) {
    if (!type) return
    if (!ws || ws.readyState !== WebSocket.OPEN) return
    const msg = {
      type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
      from: String(uid.value),
      to: String(to),
      payload: payload ?? null,
      timestamp: Date.now(),
        type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
        from: String(uid.value),
        to: String(to),
        payload: payload ?? null,
        timestamp: Date.now(),
    }
    ws.send(JSON.stringify(msg))
  }
  /* ---------------- WebRTC ---------------- */
  function createPC() {
}
/* ---------------- WebRTC ---------------- */
function createPC() {
    pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
        iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    })
    pc.onicecandidate = (e) => {
      if (e.candidate) {
        send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
      }
        if (e.candidate) {
            send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
        }
    }
    pc.ontrack = (e) => {
      log('✅ 收到远端音频流')
      const stream = e.streams[0]
      if (remoteAudio.value) {
        remoteAudio.value.srcObject = stream
        remoteAudio.value.play().catch(() => {})
      }
        log('✅ 收到远端音频流')
        const stream = e.streams[0]
        if (remoteAudio.value) {
            remoteAudio.value.srcObject = stream
            remoteAudio.value.play().catch(() => {})
        }
    }
    pc.onconnectionstatechange = () => {
      log('pc.connectionState =', pc.connectionState)
        log('pc.connectionState =', pc.connectionState)
    }
  }
  async function getLocalAudio() {
}
async function getLocalAudio() {
    if (localStream) return localStream
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
      throw new Error('getUserMedia not available')
        log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
        throw new Error('getUserMedia not available')
    }
    try {
      localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
      log('✅ 已获取本地麦克风')
      return localStream
        localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        log('✅ 已获取本地麦克风')
        return localStream
    } catch (e) {
      log('❌ 获取麦克风失败:', String(e))
      throw e
        log('❌ 获取麦克风失败:', String(e))
        throw e
    }
  }
  /** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
  async function ensureLocalTracksAdded() {
}
/** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
async function ensureLocalTracksAdded() {
    if (!pc) createPC()
    const stream = await getLocalAudio()
    const existingTrackIds = new Set(
      pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
        pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
    )
    stream.getTracks().forEach(track => {
      if (!existingTrackIds.has(track.id)) {
        pc.addTrack(track, stream)
        log('addTrack ok:', track.id)
      } else {
        // log('skip addTrack:', track.id)
      }
        if (!existingTrackIds.has(track.id)) {
            pc.addTrack(track, stream)
            log('addTrack ok:', track.id)
        } else {
            // log('skip addTrack:', track.id)
        }
    })
    return stream
  }
  async function flushCandidates() {
}
async function flushCandidates() {
    if (!pc || !pc.remoteDescription) return
    while (pendingCandidates.length) {
      const c = pendingCandidates.shift()
      try {
        await pc.addIceCandidate(new RTCIceCandidate(c))
      } catch (e) {
        log('addIceCandidate failed:', String(e))
      }
        const c = pendingCandidates.shift()
        try {
            await pc.addIceCandidate(new RTCIceCandidate(c))
        } catch (e) {
            log('addIceCandidate failed:', String(e))
        }
    }
  }
  /* ---------------- 信令处理 ---------------- */
  async function onSignal(msg) {
}
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
    if (!msg) return
    const t = (msg.type || '').toString()
    // ✅ 忽略后端回显给自己的消息(你的后端会回显 sender)
    if (msg.from && String(msg.from) === String(uid.value)) {
      return
        return
    }
    // system 消息:login/pong/busy 仅记录
    if (msg.from === 'system') {
      log('system msg:', msg)
      return
    }
    if (!t) return
    if (t === 'call') {
      // 收到来电:弹接听按钮
      incomingFrom = String(msg.from)
      peerUid.value = incomingFrom
      acceptedByMe = false
      offeredByPeer = false
      state.value = 'ringing'
      log('📞 收到来电 call,来自', incomingFrom)
      return
    }
    if (t === 'accept') {
      // 主叫收到 accept:开始 offer
      log('✅ 对方接听 accept,开始建立连接(offer)')
      await startOffer()
      return
    }
    if (t === 'busy') {
      log('❌ 对方忙线/拒绝')
      cleanup(false)
      return
    }
    if (t === 'offer') {
      // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
      offeredByPeer = true
      incomingFrom = String(msg.from)
      peerUid.value = incomingFrom
      if (!acceptedByMe) {
        state.value = 'ringing'
        log('📩 收到 offer(但未接听),已等待用户点击接听')
        // 暂存 offer 到一个变量
        lastOfferSdp = msg.payload?.sdp
        log('system msg:', msg)
        return
      }
      // 已接听:直接 answer
      await answerOffer(msg.payload?.sdp, incomingFrom)
      return
    }
    if (!t) return
    if (t === 'call') {
        // 收到来电:弹接听按钮
        incomingFrom = String(msg.from)
        peerUid.value = incomingFrom
        acceptedByMe = false
        offeredByPeer = false
        state.value = 'ringing'
        log('📞 收到来电 call,来自', incomingFrom)
        return
    }
    if (t === 'accept') {
        // 主叫收到 accept:开始 offer
        log('✅ 对方接听 accept,开始建立连接(offer)')
        await startOffer()
        return
    }
    if (t === 'busy') {
        log('❌ 对方忙线/拒绝')
        cleanup(false)
        return
    }
    if (t === 'offer') {
        // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
        offeredByPeer = true
        incomingFrom = String(msg.from)
        peerUid.value = incomingFrom
        if (!acceptedByMe) {
            state.value = 'ringing'
            log('📩 收到 offer(但未接听),已等待用户点击接听')
            // 暂存 offer 到一个变量
            lastOfferSdp = msg.payload?.sdp
            return
        }
        // 已接听:直接 answer
        await answerOffer(msg.payload?.sdp, incomingFrom)
        return
    }
    if (t === 'answer') {
      log('✅ 收到 answer,通话建立')
      if (!pc) createPC()
      await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
      await flushCandidates()
      state.value = 'in-call'
      startTimer()
      return
        log('✅ 收到 answer,通话建立')
        if (!pc) createPC()
        await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
        await flushCandidates()
        state.value = 'in-call'
        startTimer()
        return
    }
    if (t === 'candidate') {
      const c = msg.payload?.candidate
      if (!c) return
      if (pc && pc.remoteDescription) {
        await pc.addIceCandidate(new RTCIceCandidate(c))
      } else {
        pendingCandidates.push(c)
      }
      return
        const c = msg.payload?.candidate
        if (!c) return
        if (pc && pc.remoteDescription) {
            await pc.addIceCandidate(new RTCIceCandidate(c))
        } else {
            pendingCandidates.push(c)
        }
        return
    }
    if (t === 'hangup') {
      log('📴 对方挂断')
      cleanup(false)
      return
        log('📴 对方挂断')
        cleanup(false)
        return
    }
    if (t === 'pong') {
      // 心跳响应
      // log('pong')
      return
        // 心跳响应
        // log('pong')
        return
    }
  }
  // 暂存 offer(用于“先点接听再 answer”)
  let lastOfferSdp = null
  /* ---------------- 业务操作 ---------------- */
  function connectWS() {
}
// 暂存 offer(用于“先点接听再 answer”)
let lastOfferSdp = null
/* ---------------- 业务操作 ---------------- */
function connectWS() {
    if (ws) ws.close()
    ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
    ws.onopen = () => {
      connected.value = true
      log('WS 已连接 userId=', uid.value)
      startPing()
        connected.value = true
        log('WS 已连接 userId=', uid.value)
        startPing()
    }
    ws.onclose = () => {
      connected.value = false
      log('WS 已关闭')
      stopPing()
        connected.value = false
        log('WS 已关闭')
        stopPing()
    }
    ws.onerror = (e) => log('WS error', e)
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data)
      log('收到信令', msg)
      onSignal(msg)
        const msg = JSON.parse(e.data)
        log('收到信令', msg)
        onSignal(msg)
    }
  }
  function requestCall() {
}
function requestCall() {
    state.value = 'calling'
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    send('call', peerUid.value, null)
    log('➡️ 发起呼叫 call 给', peerUid.value)
  }
  function acceptCall() {
}
function acceptCall() {
    if (!incomingFrom) return
    acceptedByMe = true
    // 先告诉对方我接听了(后端会把双方置忙)
    send('accept', incomingFrom, null)
    log('✅ 已点击接听,发送 accept 给', incomingFrom)
    state.value = 'calling'
    // 如果 offer 已经提前到达(我们暂存了),立刻 answer
    if (lastOfferSdp) {
      answerOffer(lastOfferSdp, incomingFrom)
      lastOfferSdp = null
        answerOffer(lastOfferSdp, incomingFrom)
        lastOfferSdp = null
    }
  }
  function rejectCall() {
}
function rejectCall() {
    if (!incomingFrom) return
    // 你后端没有 reject,用 busy 表示拒绝/不可接听
    send('busy', incomingFrom, null)
    log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    state.value = 'idle'
  }
  async function startOffer() {
}
async function startOffer() {
    if (!pc) createPC()
    // ✅ 不会重复 addTrack
    await ensureLocalTracksAdded()
    const offer = await pc.createOffer()
    await pc.setLocalDescription(offer)
    send('offer', peerUid.value, { sdp: pc.localDescription })
    log('➡️ 已发送 offer 给', peerUid.value)
  }
  async function answerOffer(offerSdp, from) {
}
async function answerOffer(offerSdp, from) {
    if (!offerSdp) {
      log('❌ offer sdp 为空,无法接听')
      return
        log('❌ offer sdp 为空,无法接听')
        return
    }
    if (!pc) createPC()
    await pc.setRemoteDescription(new RTCSessionDescription(offerSdp))
    await flushCandidates()
    await ensureLocalTracksAdded()
    const answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)
    send('answer', from, { sdp: pc.localDescription })
    log('⬅️ 已发送 answer 给', from)
    state.value = 'in-call'
    startTimer()
  }
  function hangup() {
}
function hangup() {
    cleanup(true)
  }
  /* ---------------- 清理 ---------------- */
  function cleanup(sendToPeer) {
}
/* ---------------- 清理 ---------------- */
function cleanup(sendToPeer) {
    try {
      if (sendToPeer) send('hangup', peerUid.value, null)
        if (sendToPeer) send('hangup', peerUid.value, null)
    } catch {}
    stopTimer()
    if (pc) {
      pc.getSenders().forEach(s => s.track && s.track.stop())
      pc.close()
        pc.getSenders().forEach(s => s.track && s.track.stop())
        pc.close()
    }
    if (localStream) {
      localStream.getTracks().forEach(t => t.stop())
        localStream.getTracks().forEach(t => t.stop())
    }
    pc = null
    localStream = null
    pendingCandidates.length = 0
    incomingFrom = null
    acceptedByMe = false
    offeredByPeer = false
    lastOfferSdp = null
    state.value = 'idle'
  }
  onBeforeUnmount(() => {
}
onBeforeUnmount(() => {
    cleanup(false)
    stopPing()
    if (ws) ws.close()
  })
  </script>
})
</script>
  
applications/drone-command/src/views/basicManage/deviceStock/DeviceScrapDiaLog.vue
@@ -13,6 +13,7 @@
                        <el-input class="command-input" v-model="formData.disposeWay" maxlength="200" type="textarea" placeholder="请输入" clearable />
                    </el-form-item>
                </el-col>
                <div v-if="checkAreaBind" class="warning-text"> 注:该设备已绑定区域,报废设备该区域无法正常使用,是否报废?</div>
            </el-row>
        </el-form>
        <template #footer>
@@ -35,7 +36,7 @@
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { fieldRules } from '@ztzf/utils'
import { fwDeviceScrapSaveApi } from '@/views/basicManage/deviceStock/fwDeviceScrapApi'
import { fwDeviceScrapSaveApi,fivDevicelcheckAreaBindApi } from '@/views/basicManage/deviceStock/fwDeviceScrapApi'
import dayjs from 'dayjs'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
@@ -105,9 +106,29 @@
    dialogMode.value = mode || 'add'
    visible.value = true
    formData.value.deviceId = row.id
    getCheckAreaBind()
}
const checkAreaBind = ref(false)
function getCheckAreaBind() {
    fivDevicelcheckAreaBindApi({
        deviceId: formData.value.deviceId
    }).then(res => {
        checkAreaBind.value = res.data.data
    })
}
onMounted(() => {})
onMounted(() => {
})
defineExpose({ open })
</script>
<style scoped lang="scss">
.warning-text {
    width: 100%;
    color: red;
    font-size: 14px;
    text-align: center;
    margin: -10px 0 10px 0;
}
</style>
applications/drone-command/src/views/basicManage/deviceStock/fwDeviceScrapApi.js
@@ -26,3 +26,14 @@
        params,
    })
}
// 设置区域绑定校验
export const fivDevicelcheckAreaBindApi = params => {
    return request({
        url: `/drone-fw/device/fwDevice/checkAreaBind`,
        method: 'get',
        params,
    })
}
applications/drone-command/src/views/basicManage/deviceStock/index.vue
@@ -1,9 +1,9 @@
<template>
    <basic-container>
        <div class="chart">
            <DeviceChart1 />
            <DeviceChart2 />
            <DeviceChart3 />
            <DeviceChart1 :key="updateKey"/>
            <DeviceChart2 :key="updateKey"/>
            <DeviceChart3 :key="updateKey"/>
        </div>
        <el-form ref="queryParamsRef" :model="searchParams" class="command-page-history-search">
@@ -77,9 +77,9 @@
                            {{ dayjs(row.createTime).format('YYYY-MM-DD') }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="source" show-overflow-tooltip width="112" label="来源" />
                    <el-table-column prop="purpose" show-overflow-tooltip width="206" label="用途" />
                    <el-table-column prop="belongDeptName" show-overflow-tooltip label="所属部门" />
                    <el-table-column prop="source" show-overflow-tooltip width="120" label="来源" />
                    <el-table-column prop="purpose" show-overflow-tooltip width="130" label="用途" />
                    <el-table-column prop="belongDeptName" show-overflow-tooltip width="160" label="所属部门" />
                    <el-table-column prop="charger" show-overflow-tooltip width="100" label="负责人" />
                    <el-table-column prop="yxzt" show-overflow-tooltip width="116" label="运行状态">
                        <template v-slot="{ row }">
@@ -91,7 +91,7 @@
                            {{ getDictLabel(row.trackStatus, trackStatusOptions) }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" class-name="operation-btns">
                    <el-table-column label="操作" class-name="operation-btns" width="140">
                        <template v-slot="{ row }">
                            <el-link @click="handleView(row)">查看</el-link>
                            <el-link @click="handleEdit(row)">编辑</el-link>
@@ -116,9 +116,9 @@
            </div>
        </div>
        <FormDiaLog ref="dialogRef" @success="getList" />
        <DeviceTrackDiaLog ref="outboundDialogRef" @success="getList" />
        <DeviceScrapDiaLog ref="scrapDialogRef" @success="getList" />
        <FormDiaLog ref="dialogRef" @success="successFun" />
        <DeviceTrackDiaLog ref="outboundDialogRef" @success="successFun" />
        <DeviceScrapDiaLog ref="scrapDialogRef" @success="successFun" />
    </basic-container>
</template>
@@ -175,6 +175,12 @@
    { dictKey: 1, dictValue: '已出库' }
])
const updateKey = ref(0)
function successFun() {
    updateKey.value = updateKey.value + 1
    getList()
}
// 获取列表
async function getList() {
    const range = dateRangeFormat(dateRange.value)
@@ -219,7 +225,7 @@
// 归还
async function returnDevice(row) {
    await ElMessageBox.confirm(`确认归还吗?`, '提示', {
    await ElMessageBox.confirm(`确认归还吗?`, '提示', {
        type: 'warning',
        customClass: 'command-page-view-message-box',
        confirmButtonClass: 'command-message-box-confirm',
@@ -240,7 +246,7 @@
// 删除
async function handleDelete(row) {
    const tips = row ? '该条' : '选中的项'
    await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', {
    await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', {
        type: 'warning',
        customClass: 'command-page-view-message-box',
        confirmButtonClass: 'command-message-box-confirm',
applications/drone-command/src/views/basicManage/maintainRecord/FormDiaLog.vue
@@ -4,8 +4,8 @@
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-20 16:38:55
 * @FilePath     : \applications\drone-command\src\views\basicManage\maintainRecord\FormDiaLog.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-20 14:04:02
-->
<template>
@@ -102,11 +102,36 @@
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="维护计划时间" prop="planCycleValue">
                        <el-date-picker class="command-date-picker" popper-class="command-date-picker-popper"
                            v-model="formData.planCycleValue" type="date" placeholder="请选择" clearable
                            value-format="YYYY-MM-DD" :format="planCycleDateFormat" :disabled="!formData.planCycleType"
                            :disabled-date="getPlanCycleDisabledDate" :editable="false" />
                    <el-form-item label="维护计划时间" prop="planCycleValue" >
                        <el-date-picker
                            v-if="formData.planCycleType === '1'"
                            :disabled="!formData.planCycleType"
                            class="command-date-picker"
                            popper-class="command-date-picker-popper"
                            v-model="formData.planCycleValue"
                            type="date"
                            placeholder="请选择"
                            clearable
                            value-format="YYYY-MM-DD"
                            :format="planCycleDateFormat"
                            :disabled-date="getPlanCycleDisabledDate"
                            :editable="false" />
                        <el-select
                            v-else
                            :disabled="!formData.planCycleType"
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.planCycleValue"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in planCycleValueOptions"
                                :key="item.value"
                                :label="item.value"
                                :value="item.value"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
            </el-row>
@@ -145,6 +170,7 @@
dayjs.locale('zh-cn')
const planCycleOptions = inject('planCycleOptions')
const planCycleValueOptions = ref([])
const emit = defineEmits(['success'])
const formRef = ref(null) // 表单实例
@@ -186,6 +212,29 @@
    visible.value = false
}
watch(
    () => formData.value?.planCycleType,
    type => {
        planCycleValueOptions.value = []
        if (!type) return
        if (type === '1') {
            ;[...Array(12)].forEach((item, index) => {
                ;[...Array(31)].forEach((item1, index1) => {
                    planCycleValueOptions.value.push({ value: `${index + 1}月${index1 + 1}号` })
                })
            })
        } else if (type === '2') {
            ;[...Array(31)].forEach((item, index) => {
                planCycleValueOptions.value.push({ value: `${index + 1}号` })
            })
        } else if (type === '3') {
            ;['一', '二', '三', '四', '五', '六', '日'].forEach((item, index) => {
                planCycleValueOptions.value.push({ value: `星期${item}` })
            })
        }
    }
)
// 提交新增/编辑
async function handleSubmit () {
    const isValid = await formRef.value?.validate().catch(() => false)
applications/task-work-order/src/views/orderView/equipmentResource/FormDiaLog.vue
New file
@@ -0,0 +1,631 @@
<!-- 设备资源弹窗 -->
<template>
    <el-dialog
        v-model="dialogVisible"
        :title="dialogTitle"
        class="gd-dialog"
        destroy-on-close
        :close-on-click-modal="false"
    >
    <el-row class="detail-row-view" v-if="dialogType === 'view'">
      <el-col :span="12">
        <div class="label">设备类型</div>
        <div class="val">{{ getDictLabel(formData.deviceType, deviceTypeOptions) }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">设备型号</div>
        <div class="val">{{ formData.deviceName }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">设备名称</div>
        <div class="val">{{ formData.nickname }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">负载设备</div>
        <div class="val">{{ getDictLabel(formData.devicePayload, devicePayloadOptions) }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">流量剩余(GB)</div>
        <div class="val">无</div>
      </el-col>
      <el-col :span="12">
        <div class="label">流量到期时间</div>
        <div class="val">无</div>
      </el-col>
      <el-col :span="12">
        <div class="label">设备状态</div>
        <div class="val">{{ getDictLabel(formData.modeCode, dictObj.modeCode) }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">设备SN</div>
        <div class="val">{{ formData.deviceSn }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">设备位置</div>
        <div class="val">{{ formData.latitude }}, {{ formData.longitude }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">保险有效期</div>
        <div class="val">无</div>
      </el-col>
      <el-col :span="12">
        <div class="label">固件版本</div>
        <div class="val">{{ formData.firmwareVersion }}</div>
      </el-col>
       <el-col :span="12">
        <div class="label">所属部门</div>
        <div class="val">{{ formData.deviceDeptName }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">最新在线时间</div>
        <div class="val">无</div>
      </el-col>
      <el-col :span="12">
        <div class="label">设备注册时间</div>
        <div class="val">无</div>
      </el-col>
    </el-row>
        <el-form
            ref="formRef"
            :model="formData"
            :rules="rules"
            class="gd-form"
            label-width="160px"
            v-else
        >
            <el-row>
                <el-col :span="12">
                    <el-form-item label="设备类型" prop="deviceType">
                        <el-select
                            class="gd-select"
                            popper-class="gd-select-popper"
                            v-model="formData.deviceType"
                            placeholder="请选择"
                            :disabled="dialogType === 'view'"
                        >
                            <el-option
                                v-for="item in deviceTypeOptions"
                                :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="deviceName">
                        <el-input
                            class="gd-input"
                            v-model="formData.deviceName"
                            placeholder="请输入"
                            :disabled="dialogType === 'view'"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="设备名称" prop="nickname">
                        <el-input
                            class="gd-input"
                            v-model="formData.nickname"
                            placeholder="请输入"
                            :disabled="dialogType === 'view'"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="负载设备" prop="devicePayload">
                        <el-radio-group
                            v-model="formData.devicePayload"
                            :disabled="dialogType === 'view'"
                        >
                            <el-radio
                                v-for="item in devicePayloadOptions"
                                :key="item.dictKey"
                                :label="item.dictKey"
                            >
                                {{ item.dictValue }}
                            </el-radio>
                        </el-radio-group>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="设备状态" prop="modeCode">
                        <el-select
                            class="gd-select"
                            popper-class="gd-select-popper"
                            v-model="formData.modeCode"
                            placeholder="请选择"
                            :disabled="dialogType === 'view'"
                        >
                            <el-option
                                v-for="item in modeCodeOptions"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="设备SN" prop="deviceSn">
                        <el-input
                            class="gd-input"
                            v-model="formData.deviceSn"
                            placeholder="请输入"
                            :disabled="dialogType === 'view'"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="设备位置" prop="deviceLocation">
                        <el-button  class="address-btn" @click="selectLocation">
                            {{ formData.longitude }}, {{ formData.latitude }}
                        </el-button>
                    </el-form-item>
                </el-col>
                <!-- <el-col :span="12">
                    <el-form-item label="所属部门" prop="departmentId">
                        <el-tree-select
                            class="gd-select"
                            popper-class="gd-tree-select-popper"
                            v-model="formData.departmentId"
                            :data="deptTree"
                            :props="treeProps"
                            node-key="id"
                            check-strictly
                            clearable
                            @change="handleSearch"
                        />
                    </el-form-item>
                </el-col> -->
            </el-row>
        </el-form>
        <!-- <div style="width: 100%; height: 400px;">
            <CommonCesiumMap
                    ref="mapRef"
                    class="gd-cesium"
                    :active="dialogVisible"
                    :flat-mode="false"
                    :terrain="true"
                    :layer-mode="4"
                    :boundary="false"
                    :zoomToBoundary="false"
                    @ready="readyMap"
                />
        </div> -->
        <template #footer>
            <el-button color="#F2F3F5" v-if="dialogType !== 'view'" @click="handleClose">取消</el-button>
            <el-button
                v-if="dialogType !== 'view'"
                type="primary"
                color="#4C34FF"
                @click="handleSubmit"
            >
                确定
            </el-button>
        </template>
    </el-dialog>
    <el-dialog v-model="visible1"
        title="选择设备位置"
        class="gd-dialog"
        destroy-on-close
        :close-on-click-modal="false"
        @close="positionClosed"
    >
            <div style="width: 100%; height: 600px;">
            <CommonCesiumMap
                    ref="mapRef"
                    class="gd-cesium"
                    :active="visible1"
                    :flat-mode="false"
                    :terrain="true"
                    :layer-mode="4"
                    :boundary="false"
                    :zoomToBoundary="false"
                    @ready="mapReady"
                />
        </div>
            <template #footer>
                <el-button color="#F2F3F5" @click="handleMapCancel">取消</el-button>
                <el-button color="#4C34FF" type="primary" @click="handleMapConfirm">确定</el-button>
            </template>
        </el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch, inject, nextTick } from 'vue'
import { submitExternalEquipment } from './equipmentResourceApi'
import { ElMessage } from 'element-plus'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import axios from 'axios'
import _ from 'lodash'
import { getDictLabel,fieldRules } from '@ztzf/utils'
const props = defineProps({
    modelValue: {
        type: Boolean,
        default: false
    },
    dialogType: {
        type: String,
        default: 'add', // add, edit, view
        validator: (value) => ['add', 'edit', 'view'].includes(value)
    },
    equipmentData: {
        type: Object,
        default: () => ({})
    }
})
const dictObj = inject('dictObj')
const visible1 = ref(false) // 弹框显隐
const mapRef = ref(null)
const tempLocation = ref({ longitude: null, latitude: null })
const deptTree = ref([]) // 部门树
const treeProps = {
    label: 'name',
    children: 'children',
}
// 设备类型(0机场,1无人机)
const modeCodeOptions = ref([
    { dictKey: 0, dictValue: '在线' },
    { dictKey: 1, dictValue: '离线' }
])
// 设备类型(0机场,1无人机)
const deviceTypeOptions = ref([
    { dictKey: 0, dictValue: '机场' },
    { dictKey: 1, dictValue: '无人机' }
])
// 设备负载(0红外,1喇叭,2闪光灯)
const devicePayloadOptions = ref([
    { dictKey: '红外', dictValue: '红外' },
    { dictKey: '喇叭', dictValue: '喇叭' },
    { dictKey: '闪光灯', dictValue: '闪光灯' }
])
const emit = defineEmits(['update:modelValue', 'confirm'])
const dialogVisible = computed({
    get: () => props.modelValue,
    set: (value) => emit('update:modelValue', value)
})
const dialogTitle = computed(() => {
    switch (props.dialogType) {
        case 'add':
            return '添加'
        case 'edit':
            return '编辑'
        case 'view':
            return '查看'
        default:
            return '设备'
    }
})
const formRef = ref(null)
const initForm = () => ({
    childSn: '', // 子设备(一般用途是机场下面的无人机)
    deviceName: '', // 设备型号
    devicePayload: '', // 设备负载(红外,喇叭,闪光灯)
    deviceSn: '', // 设备编码
    deviceType: null, // 设备类型(0机场,1无人机)
    firmwareVersion: '', //    固件版本
    insureExpiredTime: '', // 保险过期时间yyyy-MM-dd HH:mm:ss
    latitude: 0, // 当前纬度
    longitude: 0, // 当前经度
    modeCode: null, // 设备状态:{0:"在线",1:"离线"}
    nickname: '', // 设备昵称
    trafficExpireTime: '', // 流量到期时间yyyy-MM-dd HH:mm:ss
    trafficRemaining: '', // 剩余流量(单位:GB)
    address: '', // 设备位置
})
const formData = ref(initForm())
const rules = {
    deviceType: fieldRules(true),
    deviceModel: fieldRules(true, 50),
    deviceName: fieldRules(true, 50),
    deviceSn: fieldRules(true, 50),
    deviceType: fieldRules(true),
    modeCode: fieldRules(true),
    nickname: fieldRules(true, 50),
}
// const rules = {
//     deviceType: [
//         { required: true, message: '请选择', trigger: 'change' }
//     ],
//     deviceModel: [
//         { required: true, message: '请输入', trigger: 'blur' }
//     ],
//     deviceName: [
//         { required: true, message: '请输入', trigger: 'blur' }
//     ],
//     deviceStatus: [
//         { required: true, message: '请选择', trigger: 'change' }
//     ],
//     deviceSn: [
//         { required: true, message: '请输入', trigger: 'blur' }
//     ]
// }
let viewer
let map
let redPointEntity
let leftClickBound = false
function setMapPoint (longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE,
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.6),
                backgroundPadding: new Cesium.Cartesian2(5, 5),
                pixelOffset: new Cesium.Cartesian2(0, -20),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20),
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
async function selectLocation () {
    visible1.value = true
    if (viewer) return
    await nextTick()
    map = mapRef.value?.getMap()
    viewer = map?.viewer || null
    if (viewer && !leftClickBound) {
        map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
        leftClickBound = true
    }
    if (formData.value.longitude != null && formData.value.latitude != null) {
        tempLocation.value = { longitude: formData.value.longitude, latitude: formData.value.latitude }
        setMapPoint(formData.value.longitude, formData.value.latitude)
    }
}
async function getLocationName () {
    const tk = import.meta.env.VITE_APP_TDT_TOKEN
    const { latitude, longitude } = tempLocation.value
    const http = axios.create({ withCredentials: false })
    const res = await http({
        url: `https://api.tianditu.gov.cn/geocoder?postStr={'lon':${longitude},'lat':${latitude},'ver':1}&tk=${tk}`,
        method: 'get',
    })
    formData.value.address = res.data.result.formatted_address
}
function handleMapConfirm () {
    if (tempLocation.value.longitude != null && tempLocation.value.latitude != null) {
        formData.value.longitude = _.round(tempLocation.value.longitude, 6)
        formData.value.latitude = _.round(tempLocation.value.latitude, 6)
        getLocationName()
    }
    visible1.value = false
}
function handleMapCancel () {
    tempLocation.value = { longitude: null, latitude: null }
    visible1.value = false
}
function LeftClickEvent (click) {
    const pos = click.position // Cartesian2 屏幕坐标
    // 屏幕坐标 -> 椭球面坐标(不考虑地形/模型)
    const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    if (!cartesian3) {
        return { longitude: 112, latitude: 23 }
    }
    // 椭球面坐标 -> 经纬度
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Cesium.Math.toDegrees(carto.longitude)
    const latitude = Cesium.Math.toDegrees(carto.latitude)
    tempLocation.value = { longitude, latitude }
    // 添加红点(全局仅保留一个)并渲染经纬度标签
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                color: Cesium.Color.RED,
                pixelSize: 10,
            },
            label: {
                text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`,
                font: '14px',
                fillColor: Cesium.Color.WHITE, // 文字颜色:白色
                backgroundColor: Cesium.Color.BLACK.withAlpha(0.6), //背景颜色
                backgroundPadding: new Cesium.Cartesian2(5, 5), // 水平/垂直内边距(像素)
                pixelOffset: new Cesium.Cartesian2(0, -20), // 可选:微调位置
                showBackground: true, // 确保背景显示(某些版本需要)
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 负值更靠近相机(显示在前)
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
function positionClosed () {
    // 清除地图相关数据
    if (viewer && leftClickBound) {
        map.publicCesium?.removeLeftClickEvent?.(null, LeftClickEvent)
        leftClickBound = false
    }
    redPointEntity = null
    viewer = null
}
// 监听设备数据变化,更新表单
watch(
    () => props.equipmentData,
    (newData) => {
        if (newData) {
            Object.assign(formData.value, newData)
        } else {
            formData.value = initForm()
        }
    },
    { deep: true, immediate: true }
)
// 处理关闭
function handleClose() {
    dialogVisible.value = false
    positionClosed()
    resetForm()
}
// 重置表单
function resetForm() {
    formRef.value?.resetFields()
    // 重置为默认值
        Object.assign(formData.value, {
            childSn: '', // 子设备(一般用途是机场下面的无人机)
            deviceName: '', // 设备型号f
            devicePayload: '', // 设备负载(红外,喇叭,闪光灯)
            deviceSn: '', // 设备编码
            deviceType: null, // 设备类型(0机场,1无人机)
            firmwareVersion: '', //    固件版本
            insureExpiredTime: '', // 保险过期时间yyyy-MM-dd HH:mm:ss
            latitude: 0, // 当前纬度
            longitude: 0, // 当前经度
            modeCode: null, // 设备状态:{0:"在线",1:"离线"}
            nickname: '', // 设备昵称
            trafficExpireTime: '', // 流量到期时间yyyy-MM-dd HH:mm:ss
            trafficRemaining: '', // 剩余流量(单位:GB)
            address: '', // 设备位置
            departmentId: '', // 所属部门
        })
}
// 处理提交
async function handleSubmit() {
    const isValid = await formRef.value?.validate()
    if (!isValid) {
        return
    }
    try {
        if (props.dialogType === 'add') {
            await submitExternalEquipment(formData.value)
            ElMessage.success('添加成功')
        } else if (props.dialogType === 'edit') {
            await submitExternalEquipment(formData.value)
            ElMessage.success('编辑成功')
        }
        emit('confirm')
        dialogVisible.value = false
        resetForm()
    } catch (error) {
        ElMessage.error('操作失败')
    }
}
// 地图加载完毕
function mapReady() {
    mapRef.value.flyBoundary()
}
// 初始化地图实例
function initMap() {
    if (viewer) return
    const map = mapRef.value?.getMap()
    viewer = map?.viewer || null
}
</script>
<style scoped lang="scss">
.gd-form {
    .el-form-item {
        margin-bottom: 20px;
    }
}
.command-page-map-view-dialog {
    margin: 0;
    padding: 20px;
    display: flex;
    flex-direction: column;
    width: 80%;
    height: 812px;
    background: #1A1A2A;
    border-radius: 6px;
    border: 1px solid #2E2E46;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    .el-dialog__header,
    .el-dialog__footer {
        margin: 0;
        padding: 0;
    }
    .el-dialog__footer {
        margin-top: 20px;
        text-align: center;
        .el-button {
            margin: 0 10px;
        }
    }
    .el-dialog__body {
        display: flex;
        flex-direction: column;
        height: 100%;
        min-height: 600px;
        padding: 0;
        .dialog-container {
            display: flex;
            height: 100%;
            .left-container {
                width: 100%;
                height: 100%;
                .leftMap {
                    width: 100%;
                    height: 100%;
                    min-height: 600px;
                }
            }
        }
    }
}
</style>
applications/task-work-order/src/views/orderView/equipmentResource/index.vue
New file
@@ -0,0 +1,237 @@
<!-- 设备资源管理 -->
<template>
    <basic-container>
        <el-form ref="queryParamsRef" :model="searchParams" class="gd-search-form">
            <el-form-item label="设备名称" prop="nickname">
                <el-input
                    class="gd-input gray"
                    v-model="searchParams.nickname"
                    placeholder="请输入设备名称"
                    clearable
                    @clear="handleSearch"
                />
            </el-form-item>
            <el-form-item label="设备类型" prop="deviceType">
                <el-select
                    class="gd-select gray"
                    popper-class="gd-select-popper"
                    v-model="searchParams.deviceType"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in deviceTypeOptions"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item label="设备状态" prop="modeCode">
                <el-select
                    class="gd-select gray"
                    popper-class="gd-select-popper"
                    v-model="searchParams.modeCode"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.modeCode"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </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="addEquipment">新增</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">
                    <el-table-column type="index" width="64" label="序号" />
                    <el-table-column prop="deviceType" show-overflow-tooltip label="设备类型">
                        <template v-slot="{ row }">
                            {{ row.deviceType === '1' ? '无人机' : '机场' }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceName" show-overflow-tooltip label="设备型号" />
                    <el-table-column prop="nickname" show-overflow-tooltip label="设备名称" />
                    <el-table-column prop="devicePayload" show-overflow-tooltip label="负载设备" />
                    <el-table-column prop="modeCode" show-overflow-tooltip label="设备状态">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.modeCode, dictObj.modeCode) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="deviceSn" show-overflow-tooltip label="设备SN" />
                    <el-table-column show-overflow-tooltip label="设备位置" >
                        <template v-slot="{ row }">
                            {{ row.latitude ? (row.latitude + ', ' + row.longitude) : '未定位' }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="firmwareVersion" show-overflow-tooltip label="固件版本" />
                    <el-table-column prop="deviceDeptName" show-overflow-tooltip label="所属部门" />
                    <el-table-column label="操作" class-name="operation-btns" width="180">
                        <template v-slot="{ row }">
                            <el-link @click="viewEquipment(row)">查看</el-link>
                            <!-- <el-link type="primary" @click="editEquipment(row)">编辑</el-link>
                            <el-link type="danger" @click="deleteEquipment(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>
        <!-- 设备资源弹窗 -->
        <EquipmentResourceDialog
            ref="equipmentResourceDialogRef"
            v-model="dialogVisible"
            :dialog-type="dialogType"
            :equipment-data="equipmentData"
            @confirm="handleConfirm"
        />
    </basic-container>
</template>
<script setup>
import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
import { onMounted, ref, nextTick } from 'vue'
import { getDictionaryByCode } from '@/api/system/dictbiz'
import { getDictLabel } from '@ztzf/utils'
import { getEquipmentPage, removeEquipment } from './equipmentResourceApi'
import EquipmentResourceDialog from './FormDiaLog.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 初始化查询参数
const initSearchParams = () => ({
    nickname: '', // 设备名称
    deviceType: '', // 设备类型
    modeCode: '', // 设备状态
    current: 1, // 当前页
    size: 10, // 每页大小
})
const searchParams = ref(initSearchParams()) // 查询参数
const total = ref(0) // 总条数
const loading = ref(true) // 列表加载中
const list = ref([]) // 列表数据
const queryParamsRef = ref(null) // 查询表单实例
const equipmentResourceDialogRef = ref(null)
const dialogVisible = ref(false)
const dialogType = ref('add') // add, edit, view
const equipmentData = ref({})
// 设备类型(0机场,1无人机)
const deviceTypeOptions = ref([
    { dictKey: 0, dictValue: '机场' },
    { dictKey: 1, dictValue: '无人机' }
])
const dictObj = ref({
    modeCode: []
})
provide('dictObj', dictObj)
// 获取列表
async function getList() {
    loading.value = true
    try {
        const res = await getEquipmentPage(searchParams.value)
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
    } finally {
        loading.value = false
    }
}
// 查询
function handleSearch() {
    searchParams.value.current = 1
    getList()
}
// 重置查询
function resetForm() {
    queryParamsRef.value?.resetFields()
    searchParams.value.current = 1
    getList()
}
// 添加设备
function addEquipment() {
    dialogType.value = 'add'
    equipmentData.value = {}
    dialogVisible.value = true
}
// 查看设备
function viewEquipment(row) {
    dialogType.value = 'view'
    equipmentData.value = { ...row }
    dialogVisible.value = true
}
// 编辑设备
function editEquipment(row) {
    dialogType.value = 'edit'
    equipmentData.value = { ...row }
    dialogVisible.value = true
}
// 删除设备
async function deleteEquipment(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 removeEquipment({ ids: ids })
    ElMessage.success('删除成功')
    getList()
}
// 处理弹窗确认
function handleConfirm() {
    dialogVisible.value = false
    getList()
}
// 获取字典
function getDictList() {
    getDictionaryByCode('modeCode').then(res => {
        dictObj.value = res.data.data
    })
}
onMounted(() => {
    getDictList()
    getList()
})
</script>
<style scoped lang="scss"></style>
applications/task-work-order/src/views/orderView/flyingHand/FlyingHandDialog.vue
@@ -30,7 +30,7 @@
      </el-col>
      <el-col :span="12">
        <div class="label">擅长任务类型</div>
        <div class="val">{{ getDictLabel(formData.skilledTaskType, dictObj.skilledTaskType) }}</div>
        <div class="val">{{ getDictLabel(formData.skilledTaskType, dictObj.patrolTaskType) }}</div>
      </el-col>
      <el-col :span="12">
        <div class="label">项目经验</div>
@@ -149,7 +149,7 @@
                            :disabled="dialogType === 'view'"
                        >
                            <el-option
                                v-for="item in dictObj.skilledTaskType"
                                v-for="item in dictObj.patrolTaskType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
applications/task-work-order/src/views/orderView/flyingHand/index.vue
@@ -72,7 +72,7 @@
                    </el-table-column>
                    <el-table-column prop="skilledTaskType" show-overflow-tooltip label="擅长任务类型">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.skilledTaskType, dictObj.skilledTaskType) }}
                            {{ getDictLabel(row.skilledTaskType, dictObj.patrolTaskType) }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" class-name="operation-btns" width="180">
@@ -136,7 +136,7 @@
const flyingHandData = ref({})
const dictObj = ref({
    skilledTaskType: [], // 擅长任务类型
    patrolTaskType: [], // 擅长任务类型
    skilledUavType: [], // 擅长机型
    technicalStrength: [] // 技术特长
})
@@ -212,7 +212,7 @@
// 获取字典
function getDictList() {
    getDictionaryByCode('skilledTaskType,skilledUavType,technicalStrength').then(res => {
    getDictionaryByCode('patrolTaskType,skilledUavType,technicalStrength').then(res => {
        dictObj.value = res.data.data
    })
}
applications/task-work-order/src/views/orderView/orderDataManage/dataObjection/FormDiaLog.vue
@@ -292,7 +292,7 @@
    catalogResourceName: fieldRules(true),
    objectionDesc: fieldRules(true),
    objectionBasis: fieldRules(true),
    // attachId: dialogMode.value === 'add' ? fieldRules(true) : fieldRules(false)
    attachId: fieldRules(true)
}
@@ -375,6 +375,7 @@
// 提交数据的通用函数
async function submitForm(objectionStatus) {
formData.value.attachId = uploadedFiles.value.length > 0 ? uploadedFiles.value.length : ''
  const isValid = await formRef.value?.validate().catch(() => false)
  if (!isValid) return
  
@@ -504,24 +505,69 @@
    attachmentDetailsList.value = results.map(res => res.data.data).filter(Boolean)
}
// 处理附件下载
async function handleDownloadAttach() {
    if (attachmentDetailsList.value.length === 0) {
        ElMessage.warning('没有可下载的附件')
        return
    }
const handleDownloadAttach = async () => {
  if (!attachmentDetailsList.value || attachmentDetailsList.value.length === 0) {
    ElMessage.warning('没有可下载的附件')
    return
  }
    // 遍历所有附件并下载
    attachmentDetailsList.value.forEach(attach => {
        if (attach.link) {
            const link = document.createElement('a')
            link.href = attach.link
            link.download = attach.originalName || attach.name
            link.style.display = 'none'
            document.body.appendChild(link)
            link.click()
            document.body.removeChild(link)
        }
    })
  // 顺序下载每个附件
  for (let i = 0; i < attachmentDetailsList.value.length; i++) {
    const attach = attachmentDetailsList.value[i]
    if (!attach.link) {
      console.warn(`附件 ${attach.originalName || attach.name} 链接无效,跳过`)
      continue
    }
    try {
      // 显示当前下载的文件信息
      const currentFileName = attach.originalName || attach.name || `附件_${i + 1}`
      const response = await fetch(attach.link)
      const blob = await response.blob()
      const url = window.URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      // 设置文件名:优先使用 originalName
      let fileName = attach.originalName || attach.name || `附件_${i + 1}`
      // 确保有扩展名
      if (fileName && !fileName.includes('.')) {
        const contentType = response.headers.get('content-type')
        if (contentType && contentType.includes('zip')) {
          fileName += '.zip'
        } else if (contentType && contentType.includes('rar')) {
          fileName += '.rar'
        } else {
          fileName += '.zip'
        }
      }
      a.download = fileName
      a.style.display = 'none'
      document.body.appendChild(a)
      a.click()
      // 清理
      setTimeout(() => {
        window.URL.revokeObjectURL(url)
        document.body.removeChild(a)
      }, 100)
      // 文件间延迟(除了最后一个)
      if (i < attachmentDetailsList.value.length - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000))
      }
    } catch (error) {
      console.error(`下载失败: ${attach.originalName || attach.name}`, error)
    }
  }
}
applications/task-work-order/src/views/orderView/orderDataManage/supplyAdd/ApplyViewDialog.vue
@@ -1,5 +1,5 @@
<template>
    <el-dialog class="gd-dialog" :close-on-click-modal="false" v-model="visible" :title="'查看'" @closed="visible = false" destroy-on-close>
    <el-dialog width="80%" class="gd-dialog" :close-on-click-modal="false" v-model="visible" :title="'查看'" @closed="visible = false" destroy-on-close>
        <div class="detail-container">
            <!-- 左侧审批记录 -->
            <div class="detail-left">
@@ -98,7 +98,7 @@
                        <el-table-column prop="fileSize" label="文件大小" />
                        <el-table-column fixed="right" label="操作" min-width="100">
                            <template #default="scope">
                                <el-button link type="text" @click="downloadFileFromTable(scope.row)">下载</el-button>
                                <el-link  type="primary"  @click="downloadFileFromTable(scope.row)">下载</el-link>
                            </template>
                        </el-table-column>
                    </el-table>
@@ -236,32 +236,52 @@
    ElMessage.error(error?.message || '文件上传失败,请重试')
}
// 查看文件
const viewFile = file => {
    if (file.link) {
        window.open(file.link, '_blank')
    } else {
        ElMessage.warning('文件链接无效')
    }
}
// 从表格下载文件 - 使用 fetch API
const downloadFileFromTable = async (file) => {
  if (!file.fileUrl) {
    ElMessage.warning('文件链接无效')
    return
  }
// 从表格下载文件
const downloadFileFromTable = file => {
    if (file.fileUrl) {
        // 创建一个隐藏的a标签来触发下载
        const a = document.createElement('a')
        a.href = file.fileUrl
        a.download = file.fileOriginalName || '下载附件'
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
    } else {
        ElMessage.warning('文件链接无效')
    }
  try {
    const response = await fetch(file.fileUrl)
    const blob = await response.blob()
    const url = window.URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    // 获取原始文件名,处理中文和特殊字符
    const originalName = file.fileOriginalName || 'download-file.zip'
    // 确保文件名有正确的扩展名
    let fileName = originalName
    if (!fileName.includes('.')) {
      // 从 Content-Type 推断扩展名
      const contentType = response.headers.get('content-type')
      if (contentType === 'application/zip') {
        fileName += '.zip'
      } else if (contentType === 'application/x-rar-compressed') {
        fileName += '.rar'
      }
    }
    a.download = fileName
    a.style.display = 'none'
    document.body.appendChild(a)
    a.click()
    // 清理
    setTimeout(() => {
      window.URL.revokeObjectURL(url)
      document.body.removeChild(a)
    }, 100)
  } catch (error) {
    console.error('下载失败:', error)
  }
}
// 下载所有文件
const downloadAllFiles = () => {
const downloadAllFiles =async () => {
    if (!tableData.value || tableData.value.length === 0) {
        ElMessage.warning('没有可下载的文件')
        return
@@ -273,22 +293,56 @@
        ElMessage.warning('所有文件链接均无效')
        return
    }
    ElMessage.success(`开始下载${validFiles.length}个文件`)
    // 依次下载每个文件(添加延迟以避免浏览器限制)
    validFiles.forEach((file, index) => {
        setTimeout(() => {
            if (file.fileUrl) {
                const a = document.createElement('a')
                a.href = file.fileUrl
                a.download = file.fileOriginalName || `下载附件_${index + 1}`
                document.body.appendChild(a)
                a.click()
                document.body.removeChild(a)
            }
        }, index * 300) // 300ms delay between downloads
    })
  // 顺序下载每个文件
  for (let i = 0; i < validFiles.length; i++) {
    const file = validFiles[i]
    try {
      const response = await fetch(file.fileUrl)
      const blob = await response.blob()
      const url = window.URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      // 设置文件名,优先使用原始文件名
      let fileName = file.fileOriginalName || `附件_${i + 1}`
      // 确保文件名有扩展名
      if (!fileName.includes('.')) {
        const contentType = response.headers.get('content-type')
        if (contentType === 'application/zip') {
          fileName += '.zip'
        } else if (contentType === 'application/x-rar-compressed') {
          fileName += '.rar'
        } else if (contentType === 'application/x-7z-compressed') {
          fileName += '.7z'
        } else {
          fileName += '.zip'
        }
      }
      a.download = fileName
      a.style.display = 'none'
      document.body.appendChild(a)
      a.click()
      // 清理资源
      setTimeout(() => {
        window.URL.revokeObjectURL(url)
        document.body.removeChild(a)
      }, 100)
      // 文件间添加延迟(除了最后一个文件)
      if (i < validFiles.length - 1) {
        await new Promise(resolve => setTimeout(resolve, 500))
      }
    } catch (error) {
      console.error(`文件下载失败: ${file.fileOriginalName || '未知文件'}`, error)
      // 继续下载下一个文件
    }
  }
}
// 根据部门ID获取部门名称
@@ -362,9 +416,6 @@
// 拒绝申请
const rejectTheApplication = () => {
    // if (uploadedFiles.value.length === 0) {
    //     return ElMessage.warning('请上传数据')
    // }
    // 打开拒绝申请弹框
    rejectionDialogVisible.value = true
}
@@ -466,6 +517,10 @@
    }
}
.el-upload__tip {
    display: inline;
}
/* 上传按钮样式 */
applications/task-work-order/src/views/orderView/orderManage/inspectionReport/index.vue
@@ -82,10 +82,9 @@
        </div>
        <el-dialog
            class="gd-dialog"
            append-to-body
            v-model="isShowEditView"
            :title="titleTxt"
            :width="pxToRem(800)"
            :width="pxToRem(500)"
            :close-on-click-modal="false"
            :destroy-on-close="true"
            @close="handleClose"
@@ -111,10 +110,12 @@
                    </el-select>
                </el-form-item>
                <el-form-item label="上传文件" prop="link" v-if="titleTxt === '上传巡查报告'">
                    {{ uploadName }}
                    <el-upload class="avatar-uploader" action="" :show-file-list="false" :before-upload="onUploadFileBefore">
                        <el-button size="small" type="primary">点击上传</el-button>
                    </el-upload>
                    <div class="upload-tip">
                        {{ uploadName }}
                    </div>
                </el-form-item>
            </el-form>
            <!-- </div> -->
@@ -311,43 +312,53 @@
        ElMessage.warning('请选择要下载的文件')
        return
    }
    if (selectedFiles.value.length === 1) {
        selectedFiles.value.forEach((file, index) => {
            // setTimeout(() => {
            if (file.link) {
                const a = document.createElement('a')
                a.href = file.link
                a.download = file.originalName || `attachment_${index + 1}`
                document.body.appendChild(a)
                a.click()
                document.body.removeChild(a)
            }
            // }, index * 300) // 300ms delay between downloads
        })
    } else {
        // 批量下载
        try {
            console.log(selectedFiles.value, '9999')
            loadingData = ElLoading.service({ background: 'rgba(0, 0, 0, 0.5)', text: '打包中,请稍等...' })
            const fileIds = selectedFiles.value.map(i => i.id)
            // const res = await getDownloadStatusApi({ type: 'dpsjzx' })
            // if (!['CANCELLED', 'COMPLETED'].includes(res.data.data?.status || 'COMPLETED')) {
            //     return ElMessage.warning('还有正在处理的')
            // }
            console.log(fileIds, '7777')
    // if (selectedFiles.value.length === 1) {
    //     selectedFiles.value.forEach(async (file, index) => {
    //         if (file.link) {
    //             try {
    //                 // 使用 fetch 获取 blob 数据来强制下载,避免浏览器打开文件
    //                 const response = await fetch(file.link)
    //                 if (!response.ok) {
    //                     throw new Error(`Failed to fetch file: ${response.status}`)
    //                 }
    //                 const blob = await response.blob()
    //                 const url = URL.createObjectURL(blob)
    //                 const a = document.createElement('a')
    //                 a.href = url
    //                 a.download = file.originalName || `attachment_${index + 1}`
    //                 document.body.appendChild(a)
    //                 a.click()
    //                 document.body.removeChild(a)
    //                 // 释放 URL 对象
    //                 setTimeout(() => URL.revokeObjectURL(url), 100)
    //                 ElMessage.success('下载成功')
    //             } catch (error) {
    //                 console.error('Download error:', error)
    //                 ElMessage.error(`下载文件 ${file.originalName} 失败: ${error.message}`)
    //             }
    //         }
    //     })
    // } else {
            try {
            const fileIds = selectedFiles.value.map(i => i.id).join(',')
            await fjDownloadByByteApi({ attachIds: fileIds }).then(res => {
                if (res.data.code === 200) {
            const blob = res.data
            const url = URL.createObjectURL(blob)
            const a = document.createElement('a')
            a.href = url
            a.download = `巡查报告.zip`
            document.body.appendChild(a)
            a.click()
            document.body.removeChild(a)
            URL.revokeObjectURL(url)
                    ElMessage.success('下载成功')
                } else {
                    ElMessage.error(res.msg || '下载失败')
                }
            })
        } catch (e) {
        } finally {
            loadingData.close()
        }
                })
            } catch (e) {
            } finally {
                // loadingData.close()
            }
    }
    // }
}
// 文档上传
applications/task-work-order/src/views/orderView/orderManage/inspectionReport/inspectionRequestApi.js
@@ -46,10 +46,11 @@
}
// 批量打包下载
export const fjDownloadByByteApi = data => {
    return request({
export const fjDownloadByByteApi = params => {
        return request({
        url: `/blade-resource/attach/downloadByByte`,
        method: 'post',
        data,
        method: 'get',
        params,
        responseType: 'blob'
    })
}
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/FormDiaLog.vue
@@ -286,6 +286,7 @@
async function getFlyerList(index, skilledTaskType) {
    const res = await gdFlyerPageApi({
        size: 999,
        current: 1,
        skilledTaskType,
    })
    flyerListMap.value[index] = res?.data?.data?.records ?? []
applications/task-work-order/src/views/orderView/orderManage/inspectionRequest/ViewDiaLog.vue
@@ -134,6 +134,7 @@
                                    v-model="formData.patrolRouteUrl"
                                    placeholder="请选择"
                                    clearable
                                    @change="getAirDetails"
                                >
                                    <el-option v-for="item in routeOptions" :key="item.id" :label="item.name" :value="item.id" />
                                </el-select>
@@ -371,7 +372,7 @@
// 获取飞手列表
async function getFlyerList() {
    const res = await gdFlyerPageApi({ size: 999 })
    const res = await gdFlyerPageApi({ size: 999,current: 1 })
    flyerList.value = res?.data?.data?.records ?? []
}
@@ -428,7 +429,14 @@
    } finally {
    }
}
import { ArrowLineMaterialProperty } from '@/utils/cesium/Material'
let arrowLineMaterialProperty = new ArrowLineMaterialProperty({
    color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5,
})
// 获取航线详情
function getAirDetails() {
    queryAirById(formData.value.patrolRouteUrl).then(res => {
@@ -439,8 +447,8 @@
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArray(result),
                clampToGround: true,
                width: 3,
                material: Cesium.Color.RED,
                width: 4,
                material: arrowLineMaterialProperty,
            },
        })
        // 渲染起点(蓝色)
@@ -531,7 +539,20 @@
            }
  }
}
.imgBox {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    > div {
        width: calc((100% - 40px) / 5); /* 5列布局,减去4个10px间隙 */
        height: 110px;
        flex-shrink: 0;
    }
    .el-image {
        width: 100%;
        height: 100%;
    }
}
:deep(.el-timeline-item__timestamp) {
  font-size: 12px;
  color: #999;
applications/task-work-order/src/views/orderView/orderManage/orderManage/FormDiaLog.vue
@@ -265,6 +265,9 @@
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import { gdPatrolTaskPageApi } from '@/views/orderView/orderManage/inspectionRequest/inspectionRequestApi'
import { Check } from '@element-plus/icons-vue'
import droneIcon from '@/assets/images/orderView/orderManage/drone.png'
import droneActiveIcon from '@/assets/images/orderView/orderManage/droneActive.png'
// 初始化表单数据
const initForm = () => ({
    workOrderName: '',
@@ -302,8 +305,6 @@
const processList = ref([])
let viewPlane
const hasPatrolTaskList = computed(() => ['30', '40', '50', '60'].includes(String(formData.value.workOrderStatus)))
import droneIcon from '@/assets/images/orderView/orderManage/drone.png'
import droneActiveIcon from '@/assets/images/orderView/orderManage/droneActive.png'
const gdStatusObj = {
    '10': { reason: '拒绝原因', operationType: '2' },
applications/task-work-order/src/views/orderView/organizational/agenciesManagement/agenciesApi.js
@@ -56,6 +56,15 @@
  })
}
// 导出模板
export const agenciesExportTemplateApi = () => {
  return request({
    url: `/blade-system/dept/export-template`,
    method: 'get',
    responseType: 'blob',
  })
}
// 懒加载列表
export const regionLazyTreeApi = params => {
  return request({
applications/task-work-order/src/views/orderView/organizational/agenciesManagement/index.vue
@@ -48,7 +48,7 @@
    <div class="gd-table-toolbar">
      <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">新增机构</el-button>
      <el-button :icon="Upload" @click="handleImport">导入机构</el-button>
      <el-button :icon="Download" @click="handleExport">导出机构</el-button>
      <el-button :icon="Download" :loading="exportLoading" :disabled="exportLoading" @click="handleExport">导出机构</el-button>
    </div>
    <div class="gd-table-container" v-loading="loading">
@@ -105,17 +105,26 @@
      @close="handleImportClose"
    >
      <el-form class="gd-dialog-form" ref="importFormRef" :model="importParams" :rules="importRules" label-width="140px">
        <el-form-item label="导出模板">
          <el-button
            color="#F2F3F5"
            type="primary"
            @click="handleExportTemplate"
          >
            点击导出
          </el-button>
        </el-form-item>
        <el-form-item label="上传文件" prop="file">
          {{ importFileName }}
          <el-upload class="avatar-uploader" action="" :show-file-list="false" :before-upload="onImportFileBefore">
            <el-button size="small" type="primary">点击上传</el-button>
            <el-button  type="primary">点击上传</el-button>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button color="#F2F3F5" @click="isShowImportView = false">取消</el-button>
        <el-button
          class=""
          color="#4C34FF"
          :loading="importSubmitting"
          :disabled="importSubmitting"
@@ -138,6 +147,7 @@
  agenciesRemoveApi,
  agenciesImportApi,
  agenciesExportApi,
  agenciesExportTemplateApi,
  regionLazyTreeApi,
  agenciesSubmitApi
} from './agenciesApi'
@@ -183,6 +193,12 @@
  file: [{ required: true, message: '请上传文件', trigger: ['change'] }],
})
// 导出加载状态
const exportLoading = ref(false)
// 模板导出加载状态
const templateExportLoading = ref(false)
provide('regionOptions', regionOptions)
provide('dictObj', dictObj)
@@ -217,9 +233,6 @@
    resolve([])
  }
}
// 获取列表
async function getList() {
  loading.value = true
@@ -302,8 +315,32 @@
  }
}
// 导出模板
async function handleExportTemplate() {
  templateExportLoading.value = true
  try {
    const res = await agenciesExportTemplateApi()
    const blob = new Blob([res.data])
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `机构导入模板.xlsx`
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)
  } catch (error) {
    console.error('导出模板失败:', error)
  } finally {
    templateExportLoading.value = false
  }
}
// 上传文件前处理
function onImportFileBefore(file) {
  // 提示请先导出模板
  // ElMessage.warning('请先导出模板并按模板格式填写数据')
  // 检查文件类型
  const allowedTypes = ['.xlsx', '.xls']
  const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
@@ -351,8 +388,10 @@
}
// 导出机构
function handleExport() {
  agenciesExportApi(searchParams.value).then(res => {
async function handleExport() {
  exportLoading.value = true
  try {
    const res = await agenciesExportApi(searchParams.value)
    const blob = new Blob([res.data])
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
@@ -363,7 +402,11 @@
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)
  })
  } catch (error) {
    console.error('导出失败:', error)
  } finally {
    exportLoading.value = false
  }
}
onMounted(() => {
applications/task-work-order/src/views/orderView/organizational/zoningManagement/index.vue
@@ -40,7 +40,7 @@
    <div class="gd-table-toolbar">
      <el-button :icon="Plus" color="#4C34FF" type="primary" @click="openForm('add')">新增区划</el-button>
      <el-button :icon="Upload" @click="handleImport">导入区划</el-button>
      <el-button :icon="Download" @click="handleExport">导出区划</el-button>
      <el-button :icon="Download" :loading="exportLoading" :disabled="exportLoading" @click="handleExport">导出区划</el-button>
    </div>
    <div class="gd-table-container" v-loading="loading">
@@ -102,17 +102,27 @@
      @close="handleImportClose"
    >
      <el-form class="gd-dialog-form" ref="importFormRef" :model="importParams" :rules="importRules" label-width="140px">
        <el-form-item label="导出模板">
          <!-- :loading="templateExportLoading"
            :disabled="templateExportLoading" -->
          <el-button
            type="primary"
            color="#F2F3F5"
            @click="handleExportTemplate"
          >
            点击导出
          </el-button>
        </el-form-item>
        <el-form-item label="上传文件" prop="file">
          {{ importFileName }}
          <el-upload class="avatar-uploader" action="" :show-file-list="false" :before-upload="onImportFileBefore">
            <el-button size="small" type="primary">点击上传</el-button>
            <el-button type="primary">点击上传</el-button>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button color="#F2F3F5" @click="isShowImportView = false">取消</el-button>
        <el-button
          class=""
          color="#4C34FF"
          :loading="importSubmitting"
          :disabled="importSubmitting"
@@ -135,6 +145,7 @@
  zoningRemoveApi,
  zoningImportApi,
  zoningExportApi,
  zoningExportTemplateApi,
  regionLazyTreeApi,
  zoningSubmitApi
} from './zoningApi'
@@ -171,6 +182,12 @@
const importRules = ref({
  file: [{ required: true, message: '请上传文件', trigger: ['change'] }],
})
// 导出加载状态
const exportLoading = ref(false)
// 模板导出加载状态
const templateExportLoading = ref(false)
provide('regionOptions', regionOptions)
provide('dictObj', dictObj)
@@ -264,8 +281,32 @@
  }
}
// 导出模板
async function handleExportTemplate() {
  templateExportLoading.value = true
  try {
    const res = await zoningExportTemplateApi()
    const blob = new Blob([res.data])
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `区划导入模板.xlsx`
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)
  } catch (error) {
    console.error('导出模板失败:', error)
  } finally {
    templateExportLoading.value = false
  }
}
// 上传文件前处理
function onImportFileBefore(file) {
  // 提示请先导出模板
  // ElMessage.warning('请先导出模板并按模板格式填写数据')
  // 检查文件类型
  const allowedTypes = ['.xlsx', '.xls']
  const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
@@ -313,18 +354,24 @@
}
// 导出区划
function handleExport() {
  zoningExportApi(searchParams.value).then(res => {
async function handleExport() {
  exportLoading.value = true
  try {
    const res = await zoningExportApi(searchParams.value)
    const blob = new Blob([res.data])
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `区划列表${new Date().getTime()}.xlsx`
    a.download = `区划列表.xlsx`
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)
  })
  } catch (error) {
    console.error('导出失败:', error)
  } finally {
    exportLoading.value = false
  }
}
onMounted(() => {
applications/task-work-order/src/views/orderView/organizational/zoningManagement/zoningApi.js
@@ -56,6 +56,15 @@
  })
}
// 导出模板
export const zoningExportTemplateApi = () => {
  return request({
    url: `/blade-system/region/export-template`,
    method: 'get',
    responseType: 'blob',
  })
}
// 懒加载列表
export const regionLazyTreeApi = params => {
  return request({
uniapps/work-wx/src/api/index.js
@@ -175,6 +175,14 @@
  })
}
// 无人机起飞状态
export const flyActivityTakeoffStatusApi = () => {
  return request({
    url: `/system/dict/data/type/fly_activity_takeoff_status`,
    method: 'get'
  })
}
// 无人机厂商
export const manufacturerInfoApi = data => {
  return request({
uniapps/work-wx/src/pages/page/index.vue
@@ -86,7 +86,7 @@
  import docSvg from '@/static/images/doc.png'
  import lbBottomPng from '@/static/images/lb_bottom.png'
  import { sysNoticePageInfoApi, droneFlightTaskApi } from '@/api/index'
  import { flightTaskApi,flightRulesApi,flightModeApi,flightPlanApi,flyActivityStatusApi,proTypeApi} from '@/api/index'
  import { flightTaskApi,flightRulesApi,flightModeApi,flightPlanApi,flyActivityStatusApi,flyActivityTakeoffStatusApi,proTypeApi} from '@/api/index'
import { useAppStore } from "@/store";
const appStore = useAppStore()
@@ -293,6 +293,19 @@
    })
}
// 获取飞行状态
function flyActivityTakeoffStatus() {
    flyActivityTakeoffStatusApi().then(res => {
        flyActivityTakeoffStatus.value = res.data.data.map(item => ({
            ...item,
            label: item.dictLabel || '暂无',
            value: item.dictValue || '暂无',
        }))
        // 存储到store
        appStore.setFlyActivityTakeoffStatus(flyActivityTakeoffStatus.value)
    })
}
// 获取飞行器类型
const typeList = ref([])
function getProTypeApi() {
@@ -318,6 +331,7 @@
  getFlightRules()
  getFlightPlan()
  getFlyActivityStatus()
  flyActivityTakeoffStatus()
  getProTypeApi()
})
</script>
uniapps/work-wx/src/store/modules/app/index.js
@@ -16,6 +16,7 @@
    flightRules: [], // 飞行规则
    flightPlan: [], // 飞行计划
    flyActivityStatus: [], // 飞行活动状态
    flyActivityTakeoffStatus: [], // 无人机起飞状态
    proType: [], // 飞行器类型
  }),
  getters: {
@@ -41,6 +42,9 @@
    getFlyActivityStatus (state) {
      return state.flyActivityStatus
    },
    getFlyActivityTakeoffStatus (state) {
      return state.flyActivityTakeoffStatus
    },
    getProType (state) {
      return state.proType
    },
@@ -62,6 +66,9 @@
    setFlyActivityStatus (data) {
      this.flyActivityStatus = data
    },
    setFlyActivityTakeoffStatus (data) {
      this.flyActivityTakeoffStatus = data
    },
    setProType (data) {
      this.proType = data
    },
uniapps/work-wx/src/subPackages/flightApplication/details.vue
@@ -195,7 +195,54 @@
                border="none"
            ></u-input>
        </u-form-item>
        <u-popup :show="applyShow" :round="10" mode="center" @close="close" :closeable="true" :mask-close-able="true">
         <u-form labelPosition="top" :model="formParams" :rules="rules" ref="formRef">
            <u-form-item
                label="计划申请起飞时间"
                labelWidth="260rpx"
                prop="actualTakeoffTime"
                :borderBottom="true"
                ref="item1"
                @click="isShowStartTime = true"
                required
        >
            <u-input v-model="formParams.actualTakeoffTime" border="none" placeholder="请选择" suffixIcon="calendar"></u-input>
        </u-form-item>
         <u-form-item
                label="计划申请结束时间"
                labelWidth="260rpx"
                prop="actualLandingTime"
                :borderBottom="true"
                ref="item1"
                @click="isShowEndTime = true"
                required
        >
            <u-input v-model="formParams.actualLandingTime" border="none" placeholder="请选择" suffixIcon="calendar"></u-input>
        </u-form-item>
        <u-datetime-picker
            :show="isShowStartTime"
            v-model="startTime"
            mode="datetime"
            :min-date="minDate"
            :formatter="formatter"
            @confirm="onConfirmStartTime"
             @cancel="isShowStartTime = false">
        </u-datetime-picker>
        <u-datetime-picker
            :show="isShowEndTime"
            v-model="endTime"
            mode="datetime"
            :min-date="minDate"
            :formatter="formatter"
            @confirm="onConfirmEndTime"
            @cancel="isShowEndTime = false">
        </u-datetime-picker>
         </u-form>
         <u-button @click="submitApply" color="#1D6FE9">确定</u-button>
    </u-popup>
    </u-form>
    <u-button v-if="formParams.planApprovalStatus === '99'" @click="submitApply" color="#1D6FE9">申请</u-button>
</view>
</template>
<script setup>
@@ -232,17 +279,28 @@
const isShowStartTime = ref(false)
// 是否显示结束时间选择器
const isShowEndTime = ref(false)
// const startTime = ref(Date.now()); // 设置默认值为当前时间戳
// const endTime = ref(Date.now()); // 设置默认值为当前时间戳
const minDate = ref(Date.now()); // 设置最小日期为当前时间戳
const startTime = ref(Date.now()); // 设置默认值为当前时间戳
const endTime = ref(Date.now()); // 设置默认值为当前时间戳
// const onConfirmStartTime = (e) => {
//     formParams.value.flightStartTime = dayjs(e.value).format('YYYY-MM-DD HH:mm:ss')
//     isShowStartTime.value = false
// }
// const onConfirmEndTime = (e) => {
//     formParams.value.flightEndTime = dayjs(e.value).format('YYYY-MM-DD HH:mm:ss')
//     isShowEndTime.value = false
// }
const onConfirmStartTime = (e) => {
    formParams.value.actualTakeoffTime = dayjs(e.value).format('YYYY-MM-DD HH:mm')
    isShowStartTime.value = false
}
const onConfirmEndTime = (e) => {
    formParams.value.actualLandingTime = dayjs(e.value).format('YYYY-MM-DD HH:mm')
    isShowEndTime.value = false
}
const formatter = (type, value) => {
    if (type === 'year') return `${value}年`;
    if (type === 'month') return `${value}月`;
    if (type === 'day') return `${value}日`;
    if (type === 'hour') return `${value}时`;
    if (type === 'minute') return `${value}分`;
    return value;
};
@@ -306,6 +364,19 @@
    })
}
// 申请
const applyShow = ref(false)
function submitApply() {
   applyShow.value = true
}
function close() {
    // 清空时间
    formParams.value.actualTakeoffTime = ''
    formParams.value.actualLandingTime = ''
    applyShow.value = false
}
onLoad((options) => {
     flightPlanDetailsApi(options.id).then(res => {
        // return
uniapps/work-wx/src/subPackages/flightApplication/index.vue
@@ -29,10 +29,11 @@
                            {{ approvalRecord.planApprovalStatusLabel || ''}}
                        </view>
                    </view>
                    <view class="txt">计划起飞状态:{{approvalRecord.fly_activity_takeoff_status || ''}}</view>
                    <view class="txt">飞行器:{{approvalRecord.spacecraftLabel || '无'}}</view>
                    <view class="txt">飞行任务:{{approvalRecord.flightTaskTypeLabel || ''}}</view>
                    <view class="txt">飞行起飞时间:{{approvalRecord.actualTakeoffTime || ''}}</view>
                    <view class="txt">飞行降落时间:{{approvalRecord.actualLandingTime || ''}}</view>
                    <view class="txt">飞行起飞时间:{{approvalRecord.flightStartTime || ''}}</view>
                    <view class="txt">飞行降落时间:{{approvalRecord.flightEndTime || ''}}</view>
                </view>
            </u-list>
        </view>
@@ -115,6 +116,7 @@
        newData.forEach(item => {
            item.spacecraftLabel = aircraftInfoList.value.find(info => info.value === item.spacecraftId)?.label || '无'
            item.planApprovalStatusLabel = appStore.flyActivityStatus.find(status => status.dictValue === item.planApprovalStatus)?.dictLabel || '无'
            item.flyActivityTakeoffStatusLabel = appStore.flyActivityTakeoffStatus.find(status => status.dictValue === item.planStatus)?.dictLabel || '无'
            item.flightTaskTypeLabel = appStore.taskType.find(type => type.dictValue === item.flightTaskType)?.dictLabel || '无'
        })
        // 如果是第一页,直接替换数据;否则追加数据