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