Merge branch 'master' of http://139.196.74.78:10010/r/jagzwxm/ja_web
24 files modified
3 files added
| | |
| | | <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"> |
| | |
| | | <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> |
| | |
| | | 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 = { |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 加载详情 |
| | | 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) |
| | |
| | | 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 { |
| | |
| | | } |
| | | } |
| | | |
| | | 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 { |
| | |
| | | } |
| | | } |
| | | |
| | | 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}`, |
| | |
| | | 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> |
| | |
| | | <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> |
| | | |
| | |
| | | <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> |
| | |
| | | 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' |
| | |
| | | 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> |
| | |
| | | params, |
| | | }) |
| | | } |
| | | |
| | | // 设置区域绑定校验 |
| | | export const fivDevicelcheckAreaBindApi = params => { |
| | | return request({ |
| | | url: `/drone-fw/device/fwDevice/checkAreaBind`, |
| | | method: 'get', |
| | | params, |
| | | }) |
| | | } |
| | | |
| | | |
| | |
| | | <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"> |
| | |
| | | {{ 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 }"> |
| | |
| | | {{ 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> |
| | |
| | | </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> |
| | | |
| | |
| | | { dictKey: 1, dictValue: '已出库' } |
| | | ]) |
| | | |
| | | const updateKey = ref(0) |
| | | function successFun() { |
| | | updateKey.value = updateKey.value + 1 |
| | | getList() |
| | | } |
| | | |
| | | // 获取列表 |
| | | async function getList() { |
| | | const range = dateRangeFormat(dateRange.value) |
| | |
| | | |
| | | // 归还 |
| | | async function returnDevice(row) { |
| | | await ElMessageBox.confirm(`确认归还吗?`, '提示', { |
| | | await ElMessageBox.confirm(`确认归还吗?`, '提示', { |
| | | type: 'warning', |
| | | customClass: 'command-page-view-message-box', |
| | | confirmButtonClass: 'command-message-box-confirm', |
| | |
| | | // 删除 |
| | | 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', |
| | |
| | | * @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> |
| | |
| | | </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> |
| | |
| | | dayjs.locale('zh-cn') |
| | | |
| | | const planCycleOptions = inject('planCycleOptions') |
| | | const planCycleValueOptions = ref([]) |
| | | |
| | | const emit = defineEmits(['success']) |
| | | const formRef = ref(null) // 表单实例 |
| | |
| | | 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) |
| New file |
| | |
| | | <!-- 设备资源弹窗 --> |
| | | <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> |
| New file |
| | |
| | | <!-- 设备资源管理 --> |
| | | <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> |
| | |
| | | </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> |
| | |
| | | :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" |
| | |
| | | </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"> |
| | |
| | | const flyingHandData = ref({}) |
| | | |
| | | const dictObj = ref({ |
| | | skilledTaskType: [], // 擅长任务类型 |
| | | patrolTaskType: [], // 擅长任务类型 |
| | | skilledUavType: [], // 擅长机型 |
| | | technicalStrength: [] // 技术特长 |
| | | }) |
| | |
| | | |
| | | // 获取字典 |
| | | function getDictList() { |
| | | getDictionaryByCode('skilledTaskType,skilledUavType,technicalStrength').then(res => { |
| | | getDictionaryByCode('patrolTaskType,skilledUavType,technicalStrength').then(res => { |
| | | dictObj.value = res.data.data |
| | | }) |
| | | } |
| | |
| | | catalogResourceName: fieldRules(true), |
| | | objectionDesc: fieldRules(true), |
| | | objectionBasis: fieldRules(true), |
| | | // attachId: dialogMode.value === 'add' ? fieldRules(true) : fieldRules(false) |
| | | attachId: fieldRules(true) |
| | | |
| | | } |
| | | |
| | |
| | | |
| | | // 提交数据的通用函数 |
| | | 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 |
| | | |
| | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | |
| | | <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"> |
| | |
| | | <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> |
| | |
| | | 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 |
| | |
| | | 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获取部门名称 |
| | |
| | | |
| | | // 拒绝申请 |
| | | const rejectTheApplication = () => { |
| | | // if (uploadedFiles.value.length === 0) { |
| | | // return ElMessage.warning('请上传数据') |
| | | // } |
| | | // 打开拒绝申请弹框 |
| | | rejectionDialogVisible.value = true |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | .el-upload__tip { |
| | | display: inline; |
| | | } |
| | | |
| | | |
| | | |
| | | /* 上传按钮样式 */ |
| | |
| | | </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" |
| | |
| | | </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> --> |
| | |
| | | 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() |
| | | } |
| | | |
| | | } |
| | | // } |
| | | } |
| | | |
| | | // 文档上传 |
| | |
| | | } |
| | | |
| | | // 批量打包下载 |
| | | 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' |
| | | }) |
| | | } |
| | |
| | | async function getFlyerList(index, skilledTaskType) { |
| | | const res = await gdFlyerPageApi({ |
| | | size: 999, |
| | | current: 1, |
| | | skilledTaskType, |
| | | }) |
| | | flyerListMap.value[index] = res?.data?.data?.records ?? [] |
| | |
| | | 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> |
| | |
| | | |
| | | // 获取飞手列表 |
| | | async function getFlyerList() { |
| | | const res = await gdFlyerPageApi({ size: 999 }) |
| | | const res = await gdFlyerPageApi({ size: 999,current: 1 }) |
| | | flyerList.value = res?.data?.data?.records ?? [] |
| | | } |
| | | |
| | |
| | | } 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 => { |
| | |
| | | polyline: { |
| | | positions: Cesium.Cartesian3.fromDegreesArray(result), |
| | | clampToGround: true, |
| | | width: 3, |
| | | material: Cesium.Color.RED, |
| | | width: 4, |
| | | material: arrowLineMaterialProperty, |
| | | }, |
| | | }) |
| | | // 渲染起点(蓝色) |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .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; |
| | |
| | | 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: '', |
| | |
| | | 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' }, |
| | |
| | | }) |
| | | } |
| | | |
| | | // 导出模板 |
| | | export const agenciesExportTemplateApi = () => { |
| | | return request({ |
| | | url: `/blade-system/dept/export-template`, |
| | | method: 'get', |
| | | responseType: 'blob', |
| | | }) |
| | | } |
| | | |
| | | // 懒加载列表 |
| | | export const regionLazyTreeApi = params => { |
| | | return request({ |
| | |
| | | <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"> |
| | |
| | | @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" |
| | |
| | | agenciesRemoveApi, |
| | | agenciesImportApi, |
| | | agenciesExportApi, |
| | | agenciesExportTemplateApi, |
| | | regionLazyTreeApi, |
| | | agenciesSubmitApi |
| | | } from './agenciesApi' |
| | |
| | | file: [{ required: true, message: '请上传文件', trigger: ['change'] }], |
| | | }) |
| | | |
| | | // 导出加载状态 |
| | | const exportLoading = ref(false) |
| | | |
| | | // 模板导出加载状态 |
| | | const templateExportLoading = ref(false) |
| | | |
| | | provide('regionOptions', regionOptions) |
| | | provide('dictObj', dictObj) |
| | | |
| | |
| | | resolve([]) |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | // 获取列表 |
| | | async function getList() { |
| | | loading.value = true |
| | |
| | | } |
| | | } |
| | | |
| | | // 导出模板 |
| | | 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() |
| | |
| | | } |
| | | |
| | | // 导出机构 |
| | | 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') |
| | |
| | | a.click() |
| | | document.body.removeChild(a) |
| | | URL.revokeObjectURL(url) |
| | | }) |
| | | } catch (error) { |
| | | console.error('导出失败:', error) |
| | | } finally { |
| | | exportLoading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | <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"> |
| | |
| | | @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" |
| | |
| | | zoningRemoveApi, |
| | | zoningImportApi, |
| | | zoningExportApi, |
| | | zoningExportTemplateApi, |
| | | regionLazyTreeApi, |
| | | zoningSubmitApi |
| | | } from './zoningApi' |
| | |
| | | const importRules = ref({ |
| | | file: [{ required: true, message: '请上传文件', trigger: ['change'] }], |
| | | }) |
| | | |
| | | // 导出加载状态 |
| | | const exportLoading = ref(false) |
| | | |
| | | // 模板导出加载状态 |
| | | const templateExportLoading = ref(false) |
| | | |
| | | provide('regionOptions', regionOptions) |
| | | provide('dictObj', dictObj) |
| | |
| | | } |
| | | } |
| | | |
| | | // 导出模板 |
| | | 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() |
| | |
| | | } |
| | | |
| | | // 导出区划 |
| | | 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(() => { |
| | |
| | | }) |
| | | } |
| | | |
| | | // 导出模板 |
| | | export const zoningExportTemplateApi = () => { |
| | | return request({ |
| | | url: `/blade-system/region/export-template`, |
| | | method: 'get', |
| | | responseType: 'blob', |
| | | }) |
| | | } |
| | | |
| | | // 懒加载列表 |
| | | export const regionLazyTreeApi = params => { |
| | | return request({ |
| | |
| | | }) |
| | | } |
| | | |
| | | // 无人机起飞状态 |
| | | export const flyActivityTakeoffStatusApi = () => { |
| | | return request({ |
| | | url: `/system/dict/data/type/fly_activity_takeoff_status`, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 无人机厂商 |
| | | export const manufacturerInfoApi = data => { |
| | | return request({ |
| | |
| | | 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() |
| | |
| | | }) |
| | | } |
| | | |
| | | // 获取飞行状态 |
| | | 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() { |
| | |
| | | getFlightRules() |
| | | getFlightPlan() |
| | | getFlyActivityStatus() |
| | | flyActivityTakeoffStatus() |
| | | getProTypeApi() |
| | | }) |
| | | </script> |
| | |
| | | flightRules: [], // 飞行规则 |
| | | flightPlan: [], // 飞行计划 |
| | | flyActivityStatus: [], // 飞行活动状态 |
| | | flyActivityTakeoffStatus: [], // 无人机起飞状态 |
| | | proType: [], // 飞行器类型 |
| | | }), |
| | | getters: { |
| | |
| | | getFlyActivityStatus (state) { |
| | | return state.flyActivityStatus |
| | | }, |
| | | getFlyActivityTakeoffStatus (state) { |
| | | return state.flyActivityTakeoffStatus |
| | | }, |
| | | getProType (state) { |
| | | return state.proType |
| | | }, |
| | |
| | | setFlyActivityStatus (data) { |
| | | this.flyActivityStatus = data |
| | | }, |
| | | setFlyActivityTakeoffStatus (data) { |
| | | this.flyActivityTakeoffStatus = data |
| | | }, |
| | | setProType (data) { |
| | | this.proType = data |
| | | }, |
| | |
| | | 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> |
| | |
| | | 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; |
| | | }; |
| | | |
| | | |
| | | |
| | |
| | | }) |
| | | } |
| | | |
| | | // 申请 |
| | | 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 |
| | |
| | | {{ 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> |
| | |
| | | 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 || '无' |
| | | }) |
| | | // 如果是第一页,直接替换数据;否则追加数据 |