吉安感知网项目-前端
罗广辉
2026-01-30 abfc9e567b139e1f38a8e383882ebdb7a3c0843a
feat: 场景管理
2 files modified
1 files deleted
1 files added
500 ■■■■ changed files
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue 349 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/index.vue 67 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/sceneConfigApi.js 46 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js 38 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneManage/FormDiaLog.vue
@@ -20,35 +20,33 @@
                </div>
                <div class="content" v-if="readonly">
                    <div class="detail-title">场景详情</div>
                    <div class="detail-title">场景管理详情</div>
                    <el-row>
                        <el-col :span="24">
                            <div class="label">场景名称</div>
                            <div class="val">{{ formData.sceneName }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">场景类型</div>
                            <div class="val">{{ getDictLabel(formData.sceneType, dictObj.CommandSceneType) }}</div>
                            <div class="label">防控负责人</div>
                            <div class="val">{{ formData.defenseLeader }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">设备模式</div>
                            <div class="val">{{ getDictLabel(formData.deviceMode, dictObj.deviceMode) }}</div>
                            <div class="label">防控负责人电话</div>
                            <div class="val">{{ formData.leaderPhone }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">有效时间</div>
                            <div class="val">{{ formatEffectiveDate(formData) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">指挥点位置</div>
                            <div class="val">{{ formatLocation(formData) }}</div>
                        </el-col>
                        <el-col :span="24">
                            <div class="label">关联场景配置</div>
                            <div class="val">{{ getSceneConfigName(formData.defenseSceneId) }}</div>
                        </el-col>
                    </el-row>
                    <div class="detail-title">关联区域</div>
                    <div class="command-table-container">
                        <div class="command-table-content">
                            <el-table class="command-table" ref="areaTableRef" :data="areaList" row-key="id">
                                <el-table-column prop="areaName" label="区域名称" />
                                <el-table-column prop="areaType" label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                    </div>
                </div>
                <el-form
@@ -58,7 +56,7 @@
                    :model="formData"
                    :rules="rules"
                    :disabled="readonly"
                    label-width="96px"
                    label-width="110px"
                >
                    <el-form-item label="场景名称" prop="sceneName">
                        <el-input
@@ -69,66 +67,52 @@
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="场景类型" prop="sceneType">
                    <el-form-item label="关联场景配置" prop="defenseSceneId">
                        <el-select
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.sceneType"
                            v-model="formData.defenseSceneId"
                            placeholder="请选择"
                            clearable
                        >
                            <el-option
                                v-for="item in dictObj.CommandSceneType"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                            <el-option v-for="item in sceneConfigList" :key="item.id" :label="item.sceneName" :value="item.id" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="设备模式" prop="deviceMode">
                        <el-select
                            class="command-select"
                            popper-class="command-select-popper"
                            v-model="formData.deviceMode"
                            placeholder="请选择"
                    <el-form-item label="防控负责人" prop="defenseLeader">
                        <el-input
                            class="command-input"
                            v-model="formData.defenseLeader"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        >
                            <el-option
                                v-for="item in dictObj.deviceMode"
                                :key="item.dictKey"
                                :label="item.dictValue"
                                :value="item.dictKey"
                            />
                        </el-select>
                        />
                    </el-form-item>
                    <div class="search-table-container">
                        <div class="search-box">
                            <div class="label">关联区域</div>
                            <el-input class="command-input" v-model="searchName" placeholder="请输入" clearable></el-input>
                        </div>
                        <el-form-item prop="areaDivideIds" label-width="0">
                            <el-table
                                class="command-table"
                                ref="areaTableRef"
                                :data="areaList.filter(item => item.areaName.includes(searchName))"
                                row-key="id"
                                @selection-change="handleAreaSelectionChange"
                            >
                                <el-table-column type="selection" width="55" :reserve-selection="true" />
                                <el-table-column prop="areaName" show-overflow-tooltip label="区域名称" />
                                <el-table-column prop="areaType" show-overflow-tooltip label="区域类型">
                                    <template v-slot="{ row }">
                                        {{ getDictLabel(row.areaType, dictObj.areaType) }}
                                    </template>
                                </el-table-column>
                            </el-table>
                        </el-form-item>
                    </div>
                    <el-form-item label="负责人电话" prop="leaderPhone">
                        <el-input
                            class="command-input"
                            v-model="formData.leaderPhone"
                            maxlength="50"
                            placeholder="请输入"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="有效时间范围" prop="effectiveDateStart">
                        <el-date-picker
                            class="command-date-picker"
                            popper-class="command-date-picker-popper"
                            v-model="effectiveDateRange"
                            type="datetimerange"
                            range-separator="至"
                            start-placeholder="开始时间"
                            end-placeholder="结束时间"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            clearable
                        />
                    </el-form-item>
                    <el-form-item label="指挥点位置" prop="longitude">
                        <div class="val">{{ formatLocation(formData) }}</div>
                    </el-form-item>
                </el-form>
                <div class="footer">
                    <el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">
                        {{ readonly ? '关闭' : '取消' }}
@@ -152,24 +136,27 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fwDefenseSceneDetailApi, fwDefenseSceneSubmitApi } from './sceneConfigApi'
import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
import { fwDefenseSceneManageDetailApi, fwDefenseSceneManageSubmitApi } from './sceneManageApi'
import { fwDefenseSceneListApi } from '../sceneConfig/sceneConfigApi'
import { fieldRules } from '@ztzf/utils'
import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
import * as Cesium from 'cesium'
import { fwAreaDivideDetailApi, fwAreaDivideListApi } from '../partition/partitionApi'
import { DrawPolygon } from '@/utils/cesium/DrawPolygon'
import commandPost from '@/assets/images/dataCockpit/legend/command-post.png'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
// 初始化表单数据
const initForm = () => ({
    sceneName: '', // 场景名称
    sceneType: '', // 场景类型
    deviceMode: '', // 设备模式
    areaDivideIds: '', // 关联区域ID
    areaCount: 0, // 区域数量
    defenseLeader: '', // 防控负责人
    leaderPhone: '', // 防控负责人电话
    effectiveDateStart: '', // 有效时间-开始
    effectiveDateEnd: '', // 有效时间-结束
    longitude: null, // 指挥点经度
    latitude: null, // 指挥点纬度
    defenseSceneId: '', // 关联场景配置ID
})
const emit = defineEmits(['success'])
@@ -180,22 +167,22 @@
const submitting = ref(false) // 提交中
const readonly = computed(() => dialogMode.value === 'view')
const titleEnum = ref({ edit: '编辑', view: '查看', add: '新增' })
const areaTableRef = ref(null)
const searchName = ref('')
const areaList = ref([]) // 关联区域列表
const selectedAreaRows = ref([]) //选中列表
const dictObj = inject('dictObj')
const effectiveDateRange = ref([]) // 有效时间范围
const sceneConfigList = ref([]) // 场景配置列表
const mapRef = ref(null)
const route = useRoute()
let viewer
let redPointEntity
let leftClickBound = false
// 表单验证规则
const rules = {
    sceneName: fieldRules(true, 50),
    sceneType: fieldRules(true),
    deviceMode: fieldRules(true),
    areaDivideIds: fieldRules(false),
    defenseLeader: fieldRules(true, 50),
    leaderPhone: fieldRules(true, 50),
    effectiveDateStart: fieldRules(true),
    longitude: fieldRules(true),
    defenseSceneId: fieldRules(true),
}
// 关闭弹框
@@ -209,13 +196,10 @@
    if (!isValid) return
    submitting.value = true
    try {
        const ids = selectedAreaRows.value.map(item => item.id)
        formData.value.areaDivideIds = ids.join(',')
        formData.value.areaCount = ids.length
        await fwDefenseSceneSubmitApi(formData.value)
        await fwDefenseSceneManageSubmitApi(formData.value)
        saveOperationLog({
            requestUri: route.path,
            title: `${route.name || '场景配置管理'}-${dialogMode.value === 'add' ? '新增' : '编辑'}`,
            title: `${route.name || '场景管理'}-${dialogMode.value === 'add' ? '新增' : '编辑'}`,
            type: 1,
        })
        ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
@@ -226,128 +210,135 @@
    }
}
// 监听有效时间范围变化,同步到表单数据
watch(
    () => effectiveDateRange.value,
    () => {
        formData.value.effectiveDateStart = effectiveDateRange.value?.[0] || ''
        formData.value.effectiveDateEnd = effectiveDateRange.value?.[1] || ''
    }
)
// 加载详情
async function loadDetail() {
    if (!formData.value.id) return
    const res = await fwDefenseSceneDetailApi({ id: formData.value.id })
    const res = await fwDefenseSceneManageDetailApi({ id: formData.value.id })
    formData.value = res?.data?.data ?? initForm()
    // 同步有效时间范围
    effectiveDateRange.value = [formData.value.effectiveDateStart, formData.value.effectiveDateEnd].filter(Boolean)
}
// 获取区域列表
async function getAreaList() {
    if (areaList.value.length) return
    const res = await fwAreaDivideListApi({ filterSelected: 1, sceneId: formData.value.id })
    areaList.value = res?.data?.data ?? []
// 获取场景配置列表
async function getSceneConfigList() {
    if (sceneConfigList.value.length) return
    const res = await fwDefenseSceneListApi({unbound: 1})
    sceneConfigList.value = res?.data?.data ?? []
}
// 关联区域变更
async function handleAreaSelectionChange(rows) {
    const selectedRows = await ensureAreaGeom(rows)
    selectedAreaRows.value = selectedRows
    const ids = selectedRows.map(item => item.id)
    formData.value.areaDivideIds = ids.join(',')
    formData.value.areaCount = ids.length
// 获取场景配置名称
function getSceneConfigName(id) {
    if (!id) return ''
    const item = sceneConfigList.value.find(item => item.id === id)
    return item?.sceneName || id
}
// 渲染面
function renderingSurface() {
    if (!geometricSource) {
        initMap()
    }
    if (!geometricSource) return
    geometricSource && geometricSource.entities.removeAll()
    selectedAreaRows.value.forEach(item => {
        if (item.geom) {
            const pointList = geomAnalysis(item.geom)
            const result = pointList.map(item => [item.longitude, item.latitude]).flat()
            geometricSource.entities?.add({
                customType: 'control_group',
                position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
                polyline: {
                    positions: Cesium.Cartesian3.fromDegreesArray(result),
                    clampToGround: true,
                    width: 3,
                    material: Cesium.Color.RED,
                },
            })
        }
    })
// 格式化指挥点位置
function formatLocation(row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
watch(() => selectedAreaRows.value, renderingSurface)
async function ensureAreaGeom(rows) {
    const missingRows = rows.filter(row => row?.id && !row.geom)
    if (!missingRows.length) return rows
    const detailList = await Promise.all(
        missingRows.map(row =>
            fwAreaDivideDetailApi({ id: row.id })
                .then(res => res?.data?.data)
                .catch(() => null)
        )
    )
    const detailMap = new Map(detailList.filter(Boolean).map(item => [String(item.id), item]))
    if (!detailMap.size) return rows
    areaList.value = areaList.value.map(item => {
        const detail = detailMap.get(String(item.id))
        return detail ? { ...item, ...detail } : item
    })
    return rows.map(item => {
        const detail = detailMap.get(String(item.id))
        return detail ? { ...item, ...detail } : item
    })
// 格式化有效时间
function formatEffectiveDate(row) {
    if (!row?.effectiveDateStart && !row?.effectiveDateEnd) return ''
    return `${row.effectiveDateStart || '-'} ~ ${row.effectiveDateEnd || '-'}`
}
// 同步关联区域
function syncAreaSelection() {
    if (!areaTableRef.value) return
    areaTableRef.value.clearSelection()
    const rows = []
    const areaDivideIds = formData.value.areaDivideIds
    const arr = Array.isArray(areaDivideIds)
        ? areaDivideIds.map(item => String(item))
        : String(areaDivideIds || '')
                .split(',')
                .filter(Boolean)
    areaList.value.forEach(row => {
        if (arr.includes(String(row.id))) {
            areaTableRef.value.toggleRowSelection(row, true)
            rows.push(row)
        }
    })
    selectedAreaRows.value = rows
    if (!rows.length) return
    formData.value.areaCount = rows.length
}
let geometricSource
// 初始化地图实例
function initMap() {
    if (viewer) return
    const map = mapRef.value?.getMap()
    if (!map?.viewer) return
    viewer = map.viewer
    if (!geometricSource) {
        geometricSource = new Cesium.CustomDataSource('geometricSource')
        viewer.dataSources.add(geometricSource)
    if (!readonly.value && !leftClickBound) {
        map.publicCesium?.addLeftClickEvent?.(null, LeftClickEvent)
        leftClickBound = true
    }
}
// 地图左键点击事件
function LeftClickEvent(click) {
    const pos = click.position
    // 优先使用 pickPosition 获取贴地坐标(包含地形高度)
    let cartesian3 = viewer.scene.pickPosition(pos)
    // 如果 pickPosition 失败,回退到 pickEllipsoid
    if (!cartesian3 || !Cesium.defined(cartesian3)) {
        cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid)
    }
    if (!cartesian3) return
    const carto = Cesium.Cartographic.fromCartesian(cartesian3)
    const longitude = Cesium.Math.toDegrees(carto.longitude)
    const latitude = Cesium.Math.toDegrees(carto.latitude)
    formData.value.longitude = _.round(longitude, 6)
    formData.value.latitude = _.round(latitude, 6)
    setMapPoint(formData.value.longitude, formData.value.latitude)
}
// 设置地图点位
function setMapPoint(longitude, latitude) {
    if (!viewer || longitude == null || latitude == null) return
    if (!redPointEntity) {
        redPointEntity = viewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            billboard: {
                image: commandPost,
                width: 40,
                height: 56,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            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),
                showBackground: true,
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.CENTER,
                outlineWidth: 1,
                outlineColor: Cesium.Color.WHITE,
                pixelOffset: new Cesium.Cartesian2(0, -72),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        })
    } else {
        redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude)
        redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`
    }
}
// 打开弹框
async function open({ mode, row } = {}) {
    dialogMode.value = mode || 'add'
    formData.value = dialogMode.value === 'add' ? initForm() : row
    selectedAreaRows.value = []
    formData.value = dialogMode.value === 'add' ? initForm() : { ...row }
    effectiveDateRange.value = []
    redPointEntity = null
    await nextTick()
    initMap()
    await getAreaList()
    await getSceneConfigList()
    if (dialogMode.value !== 'add') {
        await loadDetail()
    }
    await nextTick()
    syncAreaSelection()
    setMapPoint(formData.value.longitude, formData.value.latitude)
}
onMounted(() => {
    getSceneConfigList()
})
defineExpose({
    open,
})
applications/drone-command/src/views/areaManage/sceneManage/index.vue
@@ -11,24 +11,6 @@
                />
            </el-form-item>
            <el-form-item label="场景类型" prop="sceneType">
                <el-select
                    class="command-select"
                    popper-class="command-select-popper"
                    v-model="searchParams.sceneType"
                    placeholder="请选择"
                    clearable
                    @change="handleSearch"
                >
                    <el-option
                        v-for="item in dictObj.CommandSceneType"
                        :key="item.dictKey"
                        :label="item.dictValue"
                        :value="item.dictKey"
                    />
                </el-select>
            </el-form-item>
            <el-form-item class="history-search-actions">
                <el-button :icon="RefreshRight" @click="resetForm"></el-button>
                <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button>
@@ -47,14 +29,19 @@
                    <el-table-column type="index" show-overflow-tooltip width="64" label="序号" />
                    <el-table-column prop="sceneName" show-overflow-tooltip label="场景名称" />
                    <el-table-column prop="sceneType" show-overflow-tooltip label="场景类型">
                    <el-table-column show-overflow-tooltip label="指挥点经度纬度">
                        <template v-slot="{ row }">
                            {{ getDictLabel(row.sceneType, dictObj.CommandSceneType) }}
                            {{ formatLocation(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="areaCount" show-overflow-tooltip label="区域数量" />
                    <el-table-column prop="counterDeviceCount" show-overflow-tooltip label="反制设备数量" />
                    <el-table-column prop="detectDeviceCount" show-overflow-tooltip label="探测设备数量" />
                    <el-table-column prop="defenseLeader" show-overflow-tooltip label="防控负责人" />
                    <el-table-column prop="leaderPhone" show-overflow-tooltip label="防控负责人电话" />
                    <el-table-column show-overflow-tooltip label="有效时间">
                        <template v-slot="{ row }">
                            {{ formatEffectiveDate(row) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="defenseSceneId" show-overflow-tooltip label="关联场景配置ID" />
                    <el-table-column label="操作" class-name="operation-btns">
                        <template v-slot="{ row }">
                            <el-link @click="openForm('view', row)">查看</el-link>
@@ -83,18 +70,16 @@
<script setup>
import { Search, RefreshRight, Plus, Delete } from '@element-plus/icons-vue'
import { nextTick, onMounted, provide, ref } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fwDefenseScenePageApi, fwDefenseSceneRemoveApi } from './sceneConfigApi'
import { fwDefenseSceneManagePageApi, fwDefenseSceneManageRemoveApi } from './sceneManageApi'
import FormDiaLog from './FormDiaLog.vue'
import { getDictionaryByCode } from '@/api/system/dictbiz'
import { getDictLabel } from '@ztzf/utils'
import { saveOperationLog } from '@ztzf/apis'
import { useRoute } from 'vue-router'
// 初始化查询参数
const initSearchParams = () => ({
    sceneName: '', // 场景名称
    sceneType: '', // 场景类型
    current: 1, // 当前页
    size: 10, // 每页大小
})
@@ -109,11 +94,11 @@
const dialogVisible = ref(false)
const route = useRoute()
// 获取列表
// 获取列表数据
async function getList() {
    loading.value = true
    try {
        const res = await fwDefenseScenePageApi(searchParams.value)
        const res = await fwDefenseSceneManagePageApi(searchParams.value)
        list.value = res?.data?.data?.records ?? []
        total.value = res?.data?.data?.total ?? 0
    } finally {
@@ -144,10 +129,10 @@
        cancelButtonClass: 'command-message-box-cancel',
    })
    const ids = row ? row.id : selectedIds.value.join(',')
    await fwDefenseSceneRemoveApi({ ids })
    await fwDefenseSceneManageRemoveApi({ ids })
    saveOperationLog({
        requestUri: route.path,
        title: `${route.name || '场景配置管理'}-删除`,
        title: `${route.name || '场景管理'}-删除`,
        type: 1,
    })
    ElMessage.success('删除成功')
@@ -160,23 +145,16 @@
    selectedIds.value = rows.map(item => item.id)
}
// 格式化指挥点位置
function formatLocation(row) {
    if (row?.longitude == null || row?.latitude == null) return ''
    return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
}
const dictObj = ref({
    sceneType: [], // 场景类型
    deviceMode: [], // 设备模式
    areaType: [], // 区域类型
})
provide('dictObj', dictObj)
// 获取字典
function getDictList() {
    getDictionaryByCode('CommandSceneType,deviceMode,areaType').then(res => {
        dictObj.value = res.data.data
    })
// 格式化有效时间
function formatEffectiveDate(row) {
    if (!row?.effectiveDateStart && !row?.effectiveDateEnd) return ''
    return `${row.effectiveDateStart || '-'} ~ ${row.effectiveDateEnd || '-'}`
}
// 新增/编辑/查看 弹框
@@ -189,7 +167,6 @@
onMounted(() => {
    getList()
    getDictList()
})
</script>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/sceneManage/sceneConfigApi.js
File was deleted
applications/drone-command/src/views/areaManage/sceneManage/sceneManageApi.js
New file
@@ -0,0 +1,38 @@
import request from '@/axios'
// 分页查询场景管理列表
export const fwDefenseSceneManagePageApi = params => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/page`,
        method: 'get',
        params: { descs: 'update_time', ...params },
    })
}
// 获取场景管理详情
export const fwDefenseSceneManageDetailApi = params => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/detail`,
        method: 'get',
        params,
    })
}
// 新增或修改场景管理
export const fwDefenseSceneManageSubmitApi = data => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/submit`,
        method: 'post',
        data,
    })
}
// 删除场景管理
export const fwDefenseSceneManageRemoveApi = params => {
    return request({
        url: `/drone-fw/area/fwDefenseSceneManage/remove`,
        method: 'post',
        params,
    })
}