9 files modified
28 files added
85 files deleted
| | |
| | | ### |
| | | # @Author : yuan |
| | | # @Date : 2026-01-07 14:58:30 |
| | | # @LastEditors : yuan |
| | | # @LastEditTime : 2026-01-16 11:02:24 |
| | | # @FilePath : \applications\task-work-order\env\.env.development |
| | | # @Description : |
| | | # Copyright 2026 OBKoro1, All Rights Reserved. |
| | | # 2026-01-07 14:58:30 |
| | | ### |
| | | NODE_ENV = 'development' |
| | | |
| | | #开发环境配置 |
| | |
| | | |
| | | #开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖) |
| | | # VITE_APP_URL = https://wrj.shuixiongit.com/api |
| | | VITE_APP_URL= http://192.168.1.33:81 |
| | | VITE_APP_URL= http://192.168.1.204 |
| | | |
| | | #新大屏地址 |
| | | VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/' |
| New file |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2026-01-09 11:14:04 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 11:35:14 |
| | | * @FilePath : \applications\drone-command\src\api\dataCockpit\index.js |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-09 11:14:04 |
| | | */ |
| | | import request from '@/axios' |
| | | |
| | | // 设备查询 |
| | | export const deviceSearchApi = (params) => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/deviceSearch', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // 历史告警/实时告警 |
| | | export const alarmLogApi = (params) => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/alarmLog', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // 告警记录统计 |
| | | export const alarmStatisticsApi = () => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/alarmStatistics', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 信号干扰/诱导驱离 |
| | | export const interferenceAndExpulsionApi = (data) => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/interferenceAndExpulsion', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // 设备统计 |
| | | export const deviceStatisticsApi = () => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/deviceStatistics', |
| | | method: 'get' |
| | | }) |
| | | } |
| New file |
| | |
| | | <template> |
| | | <div class="ztzf-cesium map-container" :id="props.containerId"></div> |
| | | <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }"> |
| | | <div class="layer-control-wrap" ref="layerWrapRef"> |
| | | <div class="layer-control" @click="toggleLayerPanel"> |
| | | <img :src="layerControlIcon" alt="图层控制" /> |
| | | </div> |
| | | <div v-if="showLayerPanel" class="layer-panel"> |
| | | <div class="panel-title">图层管理</div> |
| | | |
| | | <div class="panel-content"> |
| | | <el-tree |
| | | :data="layerTree" |
| | | show-checkbox |
| | | default-expand-all |
| | | node-key="key" |
| | | :props="layerTreeProps" |
| | | :default-checked-keys="defaultCheckedKeys" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import * as Cesium from 'cesium' |
| | | import { PublicCesium } from '@/utils/cesium/publicCesium' |
| | | import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png' |
| | | import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png' |
| | | |
| | | const props = defineProps({ |
| | | onlineDevices: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | leftCollapsed: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | containerId: { |
| | | type: String, |
| | | default: 'device-map-container', |
| | | }, |
| | | showLayerControl: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | }) |
| | | |
| | | let viewInstance = null |
| | | let viewer = null |
| | | const deviceEntityIds = new Set() |
| | | const showLayerPanel = ref(false) |
| | | const layerWrapRef = ref(null) |
| | | const layerTreeProps = { |
| | | label: 'label', |
| | | children: 'children', |
| | | } |
| | | const defaultCheckedKeys = ['global', 'city-base'] |
| | | const layerTree = ref([ |
| | | { |
| | | key: 'base', |
| | | label: '地理信息图层', |
| | | children: [ |
| | | { key: 'global', label: '全球地形' }, |
| | | { key: 'admin', label: '行政区划' }, |
| | | ], |
| | | }, |
| | | { |
| | | key: 'city', |
| | | label: '城市CIM图层', |
| | | children: [ |
| | | { key: 'city-base', label: '皖山白模' }, |
| | | { key: 'city-grid', label: '皖山白模光栅网格' }, |
| | | { key: 'city-tilt', label: '皖山倾斜摄影' }, |
| | | { key: 'city-tilt-grid', label: '皖山倾斜摄影网格' }, |
| | | ], |
| | | }, |
| | | { |
| | | key: 'sky', |
| | | label: '空域要素图层', |
| | | children: [ |
| | | { key: 'airspace', label: '空域边界' }, |
| | | { key: 'route', label: '飞行航路' }, |
| | | ], |
| | | }, |
| | | ]) |
| | | |
| | | const getDevicePosition = item => { |
| | | const longitudeRaw = item.longitude ?? item.lng ?? item.lon |
| | | const latitudeRaw = item.latitude ?? item.lat |
| | | if (longitudeRaw == null || latitudeRaw == null) return null |
| | | const longitude = Number(longitudeRaw) |
| | | const latitude = Number(latitudeRaw) |
| | | if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null |
| | | return { longitude, latitude } |
| | | } |
| | | |
| | | const clearDeviceEntities = () => { |
| | | if (!viewer) return |
| | | deviceEntityIds.forEach(id => { |
| | | viewer.entities.removeById(id) |
| | | }) |
| | | deviceEntityIds.clear() |
| | | } |
| | | |
| | | const RING_STYLES = [ |
| | | { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] }, |
| | | { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] }, |
| | | { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] }, |
| | | ] |
| | | |
| | | const MATERIAL_TYPE = 'RadialGradientMaterial' |
| | | let materialRegistered = false |
| | | |
| | | const registerRadialGradientMaterial = () => { |
| | | if (materialRegistered || !Cesium?.Material) return |
| | | materialRegistered = true |
| | | Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, { |
| | | fabric: { |
| | | type: MATERIAL_TYPE, |
| | | uniforms: { |
| | | color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0), |
| | | color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0), |
| | | }, |
| | | source: ` |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material material = czm_getDefaultMaterial(materialInput); |
| | | vec2 st = materialInput.st - vec2(0.5); |
| | | float t = clamp(length(st) * 2.0, 0.0, 1.0); |
| | | vec4 color = mix(color1, color2, t); |
| | | material.diffuse = color.rgb; |
| | | material.alpha = color.a; |
| | | return material; |
| | | } |
| | | `, |
| | | }, |
| | | translucent: () => true, |
| | | }) |
| | | } |
| | | |
| | | const buildCirclePositions = (center, radiusMeters, steps = 64) => { |
| | | const positions = [] |
| | | const latRad = Cesium.Math.toRadians(center.latitude) |
| | | const metersToLat = 1 / 111320 |
| | | const metersToLon = 1 / (111320 * Math.cos(latRad)) |
| | | for (let i = 0; i <= steps; i += 1) { |
| | | const angle = Cesium.Math.toRadians((i / steps) * 360) |
| | | const dx = radiusMeters * Math.cos(angle) |
| | | const dy = radiusMeters * Math.sin(angle) |
| | | const lon = center.longitude + dx * metersToLon |
| | | const lat = center.latitude + dy * metersToLat |
| | | positions.push(Cesium.Cartesian3.fromDegrees(lon, lat)) |
| | | } |
| | | return positions |
| | | } |
| | | |
| | | const addDeviceRings = (center, baseId) => { |
| | | RING_STYLES.forEach((ring, index) => { |
| | | const outerPositions = buildCirclePositions(center, ring.outer) |
| | | const holes = ring.inner |
| | | ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))] |
| | | : [] |
| | | const entityId = `${baseId}-ring-${index}` |
| | | deviceEntityIds.add(entityId) |
| | | registerRadialGradientMaterial() |
| | | const [startColor, endColor] = ring.gradient |
| | | const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34) |
| | | const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34) |
| | | const material = new RadialGradientMaterialProperty(color1, color2) |
| | | viewer.entities.add({ |
| | | id: entityId, |
| | | polygon: { |
| | | hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes), |
| | | material, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | class RadialGradientMaterialProperty { |
| | | constructor(color1, color2) { |
| | | this._definitionChanged = new Cesium.Event() |
| | | this.color1 = color1 |
| | | this.color2 = color2 |
| | | } |
| | | |
| | | get isConstant() { |
| | | return true |
| | | } |
| | | |
| | | get definitionChanged() { |
| | | return this._definitionChanged |
| | | } |
| | | |
| | | getType() { |
| | | return MATERIAL_TYPE |
| | | } |
| | | |
| | | getValue(time, result) { |
| | | const target = result || {} |
| | | target.color1 = this.color1 |
| | | target.color2 = this.color2 |
| | | return target |
| | | } |
| | | |
| | | equals(other) { |
| | | return ( |
| | | other instanceof RadialGradientMaterialProperty && |
| | | Cesium.Color.equals(this.color1, other.color1) && |
| | | Cesium.Color.equals(this.color2, other.color2) |
| | | ) |
| | | } |
| | | } |
| | | |
| | | const renderDeviceEntities = devices => { |
| | | if (!viewer) return |
| | | clearDeviceEntities() |
| | | devices.forEach((item, index) => { |
| | | const position = getDevicePosition(item) |
| | | if (!position) return |
| | | const entityId = `online-device-${item.id ?? index}` |
| | | deviceEntityIds.add(entityId) |
| | | addDeviceRings(position, entityId) |
| | | viewer.entities.add({ |
| | | id: entityId, |
| | | position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0), |
| | | billboard: { |
| | | image: equipmentIcon, |
| | | width: 40.34, |
| | | height: 40.34, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.onlineDevices, |
| | | devices => { |
| | | renderDeviceEntities(devices || []) |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.leftCollapsed, |
| | | isCollapsed => { |
| | | if (isCollapsed) showLayerPanel.value = false |
| | | } |
| | | ) |
| | | |
| | | const toggleLayerPanel = () => { |
| | | if (!props.showLayerControl) return |
| | | showLayerPanel.value = !showLayerPanel.value |
| | | } |
| | | |
| | | const handleClickOutside = event => { |
| | | if (!showLayerPanel.value) return |
| | | const target = event.target |
| | | if (layerWrapRef.value?.contains(target)) return |
| | | showLayerPanel.value = false |
| | | } |
| | | |
| | | onMounted(() => { |
| | | document.addEventListener('click', handleClickOutside) |
| | | viewInstance = new PublicCesium({ |
| | | dom: props.containerId, |
| | | flatMode: false, |
| | | terrain: false, |
| | | layerMode: 4, |
| | | contour: false, |
| | | }) |
| | | |
| | | viewer = viewInstance.getViewer() |
| | | renderDeviceEntities(props.onlineDevices) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | document.removeEventListener('click', handleClickOutside) |
| | | clearDeviceEntities() |
| | | if (viewInstance) { |
| | | viewInstance?.viewerDestroy() |
| | | viewInstance = null |
| | | } |
| | | |
| | | viewer = null |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .map-container { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .layer-control-root { |
| | | position: absolute; |
| | | left: 411px; |
| | | bottom: 22px; |
| | | z-index: 9; |
| | | transition: transform 0.3s ease-in-out; |
| | | pointer-events: none; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-401px); |
| | | } |
| | | } |
| | | |
| | | .layer-control-wrap { |
| | | position: relative; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | .layer-control { |
| | | width: 34px; |
| | | height: 34px; |
| | | cursor: pointer; |
| | | |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: block; |
| | | } |
| | | } |
| | | |
| | | .layer-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: absolute; |
| | | left: 46px; |
| | | bottom: 0; |
| | | width: 160px; |
| | | max-height: 442px; |
| | | background: #191932; |
| | | border-radius: 8px 8px 8px 8px; |
| | | |
| | | .panel-title { |
| | | padding: 0 16px; |
| | | line-height: 42px; |
| | | font-family: 'Open Sans', Open Sans; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #ffffff; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | border-bottom: 1px solid rgba(70, 70, 100, 0.5); |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .panel-content { |
| | | padding: 0 16px; |
| | | height: 0; |
| | | flex: 1; |
| | | overflow: auto; |
| | | } |
| | | |
| | | ::v-deep(.el-tree) { |
| | | background: transparent; |
| | | color: #c3c3dd; |
| | | font-size: 12px; |
| | | |
| | | .el-tree-node { |
| | | line-height: 30px !important; |
| | | |
| | | .el-tree-node__content { |
| | | display: flex; |
| | | align-items: center; |
| | | height: 40px !important; |
| | | line-height: 40px !important; |
| | | border-bottom: 1px solid rgba(70, 70, 100, 0.5); |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .el-tree-node__children { |
| | | .el-tree-node__content { |
| | | border: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__label) { |
| | | color: #c3c3dd; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__expand-icon) { |
| | | order: 3; |
| | | margin-left: auto; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__expand-icon.is-leaf) { |
| | | visibility: hidden; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-checkbox) { |
| | | order: 1; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__label) { |
| | | order: 2; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) { |
| | | display: none; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__content:hover) { |
| | | background: transparent !important; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) { |
| | | background: transparent !important; |
| | | color: #ffffff; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) { |
| | | color: #ffffff; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) { |
| | | background-color: #023aff; |
| | | border-color: #023aff; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-checkbox__inner) { |
| | | background-color: transparent; |
| | | border-color: #a1a3d4; |
| | | } |
| | | </style> |
| | |
| | | * @Date: 2024-10-25 15:07:51 |
| | | * @LastEditors: shuishen 1109946754@qq.com |
| | | * @LastEditTime: 2025-04-28 11:34:40 |
| | | * @FilePath: \drone-web-manage\src\components\map-container\mapContainer.vue |
| | | * @Description: |
| | | * |
| | | * Copyright (c) 2024 by shuishen, All Rights Reserved. |
| | | * @FilePath: \drone-command\src\components\map-container\mapContainer.vue |
| | | * @Description: |
| | | * |
| | | * Copyright (c) 2024 by shuishen, All Rights Reserved. |
| | | --> |
| | | <template> |
| | | <div class="map-container"> |
| | |
| | | height: 120%; |
| | | transform: translate(-50%, -50%); |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | * @Date: 2024-10-25 15:07:51 |
| | | * @LastEditors: shuishen 1109946754@qq.com |
| | | * @LastEditTime: 2025-04-28 11:34:40 |
| | | * @FilePath: \drone-web-manage\src\components\map-container\mapContainer.vue |
| | | * @FilePath: \drone-command\src\components\map-container\mapContainer.vue |
| | | * @Description: |
| | | * |
| | | * Copyright (c) 2024 by shuishen, All Rights Reserved. |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2026-01-06 09:47:27 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-16 11:12:30 |
| | | * @FilePath : \applications\task-work-order\src\config\website.js |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-06 09:47:27 |
| | | */ |
| | | /** |
| | | * 全局配置文件 |
| | | */ |
| | | export default { |
| | | title: 'saber', |
| | | logo: 'S', |
| | | key: 'saber', //配置主键,目前用于存储 |
| | | key: 'work', //配置主键,目前用于存储 |
| | | indexTitle: '', |
| | | clientId: 'drone', // 客户端id |
| | | clientSecret: 'drone_secret', // 客户端密钥 |
| | | clientId: 'saber', // 客户端id |
| | | clientSecret: 'saber_secret', // 客户端密钥 |
| | | tenantMode: true, // 是否开启租户模式 |
| | | tenantId: '000000', // 管理组租户编号 |
| | | captchaMode: true, // 是否开启验证码模式 |
| | |
| | | menu: true, |
| | | }, |
| | | fistPage: { |
| | | name: '个人工作台', |
| | | path: '/wel/index', |
| | | name: '数据驾驶舱', |
| | | path: '/dataCockpit/index', |
| | | // path: '/tickets/ticket', |
| | | |
| | | }, |
| | |
| | | i18n: 'dashboard', |
| | | }, |
| | | component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'), |
| | | }, |
| | | { |
| | | path: 'dashboard', |
| | | name: '控制台', |
| | | meta: { |
| | | i18n: 'dashboard', |
| | | menu: false, |
| | | }, |
| | | component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'), |
| | | }, |
| | | } |
| | | ], |
| | | }, |
| | | // 事件工单 |
| | |
| | | }) |
| | | } |
| | | return new Promise(resolve => { |
| | | getRoutes({tenantId,sysType: 1}).then(res => { |
| | | getRoutes({tenantId,sysType: 5}).then(res => { |
| | | const data = res.data.data; |
| | | let menu = deepClone(data); |
| | | if (!res.data?.data?.length){ |
| | |
| | | }, |
| | | GetButtons({ commit }) { |
| | | return new Promise(resolve => { |
| | | getButtons({sysType: 1}).then(res => { |
| | | getButtons({sysType: 5}).then(res => { |
| | | const data = res.data.data; |
| | | commit('SET_PERMISSION', data); |
| | | resolve(); |
| New file |
| | |
| | | <template> |
| | | <div class="center-container"> |
| | | <div class="left-box"> |
| | | <div class="logo"> |
| | | <img :src="warningLogo" alt=""> |
| | | </div> |
| | | |
| | | <div class="list"> |
| | | <div class="item" v-for="(item, ind) in warningList" :key="ind"> |
| | | <div class="val" @click="warningTypeClick(item.type)"> |
| | | {{ item.value }} |
| | | </div> |
| | | |
| | | <div class="name"> |
| | | {{ item.title }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="right-box"> |
| | | <div class="logo"> |
| | | <img :src="equipmentLogo" alt=""> |
| | | </div> |
| | | |
| | | <div class="list"> |
| | | <div class="item" v-for="(item, ind) in equipmentList" :key="ind"> |
| | | <div class="val" :class="{highlight: item.isHighlight}" @click="equipmentClick(item.isHighlight)"> |
| | | {{ item.value }} |
| | | </div> |
| | | |
| | | <div class="name"> |
| | | {{ item.title }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from 'vue' |
| | | import warningLogo from '@/assets/images/dataCockpit/warning.png' |
| | | import equipmentLogo from '@/assets/images/dataCockpit/equipment.png' |
| | | import { alarmStatisticsApi } from '@/api/dataCockpit' |
| | | |
| | | const warningList = ref([ |
| | | { |
| | | title: '实时告警', |
| | | value: 0, |
| | | type: 1 |
| | | }, |
| | | { |
| | | title: '设备告警', |
| | | value: 0, |
| | | type: 2 |
| | | }, |
| | | { |
| | | title: '历史告警', |
| | | value: 0, |
| | | type: 3 |
| | | }, |
| | | ]) |
| | | |
| | | const equipmentList = ref([ |
| | | { |
| | | title: '设备统计', |
| | | value: 0, |
| | | isHighlight: true, |
| | | }, |
| | | { |
| | | title: '无线电', |
| | | value: 0, |
| | | }, |
| | | { |
| | | title: '光电', |
| | | value: 0, |
| | | }, |
| | | { |
| | | title: '雷达', |
| | | value: 0, |
| | | } |
| | | ]) |
| | | |
| | | const emit = defineEmits(['select-warning', 'select-equipment']) |
| | | |
| | | const warningTypeClick = (type) => { |
| | | emit('select-warning', type) |
| | | } |
| | | |
| | | const equipmentClick = (isHighlight) => { |
| | | if (isHighlight) { |
| | | emit('select-equipment') |
| | | } |
| | | } |
| | | |
| | | const updateStats = async () => { |
| | | const res = await alarmStatisticsApi() |
| | | const data = res?.data?.data || {} |
| | | warningList.value = [ |
| | | { title: '实时告警', value: data.realTimeAlarmCount ?? 0, type: 1 }, |
| | | { title: '设备告警', value: data.deviceAlarmCount ?? 0, type: 2 }, |
| | | { title: '历史告警', value: data.historyAlarmCount ?? 0, type: 3 } |
| | | ] |
| | | equipmentList.value = [ |
| | | { title: '设备统计', value: data.deviceCount ?? 0, isHighlight: true }, |
| | | { title: '无线电', value: data.radioCount ?? 0 }, |
| | | { title: '光电', value: data.opticalCount ?? 0 }, |
| | | { title: '雷达', value: data.radarCount ?? 0 } |
| | | ] |
| | | } |
| | | |
| | | onMounted(() => { |
| | | updateStats() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .center-container { |
| | | display: flex; |
| | | position: absolute; |
| | | top: 75px; |
| | | left: 465px; |
| | | z-index: 9; |
| | | pointer-events: none; |
| | | |
| | | .left-box { |
| | | width: 356px; |
| | | |
| | | .list { |
| | | .item { |
| | | .val { |
| | | color: #FF4444; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .right-box { |
| | | margin-left: 20px; |
| | | width: 455px; |
| | | |
| | | .list { |
| | | .item { |
| | | .val.highlight { |
| | | color: #6382FF; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .left-box, |
| | | .right-box { |
| | | position: relative; |
| | | padding: 17px 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | height: 100px; |
| | | background: rgba(0,0,0,0.3); |
| | | box-sizing: border-box; |
| | | border-radius: 16px 16px 16px 16px; |
| | | pointer-events: auto; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: -1; |
| | | background: rgba(17, 23, 34, 0.56); |
| | | backdrop-filter: blur(2px); |
| | | border-radius: 16px 16px 16px 16px; |
| | | } |
| | | |
| | | |
| | | .logo { |
| | | img { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | } |
| | | |
| | | .list { |
| | | width: 0; |
| | | flex: 1; |
| | | height: 100%; |
| | | display: flex; |
| | | |
| | | .item { |
| | | width: 0; |
| | | flex: 1; |
| | | position: relative; |
| | | padding-left: 20px; |
| | | color: #fff; |
| | | |
| | | .name { |
| | | font-family: Open Sans, Open Sans; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .val { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 30px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 0; |
| | | right: 0; |
| | | width: 1px; |
| | | height: 100%; |
| | | width: 1px; |
| | | background: #333355; |
| | | } |
| | | |
| | | &:last-child::after { |
| | | display: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <el-dialog class="ztzf-data-cockpit-dialog" append-to-body v-model="visibleModel" :title="dialogTitle" |
| | | :close-on-click-modal="false" :destroy-on-close="true"> |
| | | <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-dialog-history-search"> |
| | | <el-form-item label="关键字" prop="keyword"> |
| | | <el-input class="ztzf-data-cockpit-search-input" v-model="searchParams.keyword" placeholder="名称/序列号" |
| | | clearable @clear="handleSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item prop="dateRange"> |
| | | <el-date-picker class="ztzf-data-cockpit-date-picker" |
| | | popper-class="ztzf-data-cockpit-tree-select-popper" v-model="searchParams.dateRange" |
| | | type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" |
| | | value-format="YYYY/MM/DD" @change="handleSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="区域" prop="area"> |
| | | <el-tree-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-tree-select-popper" |
| | | v-model="searchParams.area" :data="areaTree" :props="areaTreeProps" node-key="value" check-strictly |
| | | clearable placeholder="请选择" @change="handleSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="无人机类型" prop="droneType"> |
| | | <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="searchParams.droneType" placeholder="请选择" clearable @change="handleSearch"> |
| | | <el-option v-for="item in droneTypeOptions" :key="item.value" :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item class="history-search-actions"> |
| | | <el-button :icon="RefreshRight" @click="resetForm"></el-button> |
| | | <el-button :icon="Search" @click="handleSearch"></el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)"> |
| | | <div class="ztzf-table-content"> |
| | | <el-table class="ztzf-data-cockpit-table" :data="list"> |
| | | <el-table-column type="index" width="60" label="序号" /> |
| | | <el-table-column prop="droneName" label="无人机名称" min-width="120" /> |
| | | <el-table-column prop="droneSerialNo" label="无人机序列号" min-width="140" /> |
| | | <el-table-column label="无人机类型" min-width="110"> |
| | | <template #default="{ row }"> |
| | | {{ getDroneTypeText(row.droneType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="areaName" label="区域" min-width="90" /> |
| | | <el-table-column prop="deviceName" label="数据来源" min-width="110" /> |
| | | <el-table-column label="信号频段" min-width="100"> |
| | | <template #default="{ row }"> |
| | | {{ formatFrequency(row.signalFreqMhz) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="alarmTime" label="发现时间" min-width="150" /> |
| | | <el-table-column prop="stayDuration" label="停留时间" min-width="110" /> |
| | | <el-table-column label="反制方式" min-width="110"> |
| | | <template #default="{ row }"> |
| | | {{ getCounterWayText(row.counterWay) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="ztzf-table-pagination"> |
| | | <el-pagination popper-class="ztzf-data-cockpit-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> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search, RefreshRight } from '@element-plus/icons-vue' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { alarmLogApi } from '@/api/dataCockpit' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | device: { |
| | | type: Object, |
| | | default: () => null |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | | const visibleModel = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | const dialogTitle = computed(() => { |
| | | return `反制设备名称 ${props.device?.name || ''}`.trim() |
| | | }) |
| | | |
| | | const initSearchParams = () => ({ |
| | | keyword: '', |
| | | dateRange: [], |
| | | area: '', |
| | | droneType: '', |
| | | current: 1, |
| | | size: 10 |
| | | }) |
| | | const searchParams = ref(initSearchParams()) |
| | | const queryParamsRef = ref(null) |
| | | const loading = ref(true) |
| | | const list = ref([]) |
| | | const total = ref(0) |
| | | const areaTree = ref([ |
| | | { |
| | | label: '古村区', |
| | | value: '古村区' |
| | | }, |
| | | { |
| | | label: '新区', |
| | | value: '新区' |
| | | }, |
| | | { |
| | | label: '产业园', |
| | | value: '产业园' |
| | | } |
| | | ]) |
| | | const areaTreeProps = { |
| | | label: 'label', |
| | | children: 'children' |
| | | } |
| | | |
| | | const droneTypeOptions = [ |
| | | { label: '微型机', value: '1' }, |
| | | { label: '植保机', value: '2' } |
| | | ] |
| | | |
| | | const getList = async () => { |
| | | if (!props.device?.id) { |
| | | list.value = [] |
| | | total.value = 0 |
| | | return |
| | | } |
| | | const [startTime, endTime] = searchParams.value.dateRange || [] |
| | | const keyword = (searchParams.value.keyword || '').trim() |
| | | const params = { |
| | | alarmType: '2', |
| | | deviceId: props.device.id, |
| | | current: searchParams.value.current, |
| | | size: searchParams.value.size, |
| | | startTime, |
| | | endTime, |
| | | areaName: searchParams.value.area || undefined, |
| | | droneType: searchParams.value.droneType || undefined, |
| | | droneName: keyword || undefined, |
| | | } |
| | | loading.value = true |
| | | try { |
| | | const res = await alarmLogApi(params) |
| | | 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 () { |
| | | searchParams.value = initSearchParams() |
| | | |
| | | getList() |
| | | } |
| | | |
| | | const formatFrequency = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}MHZ` |
| | | } |
| | | |
| | | const getCounterWayText = (value) => { |
| | | if (value === 1 || value === '1') return '信号干扰' |
| | | if (value === 2 || value === '2') return '诱导驱离' |
| | | if (value === 3 || value === '3') return '无' |
| | | return value || '-' |
| | | } |
| | | |
| | | const getDroneTypeText = (value) => { |
| | | if (value === 1 || value === '1') return '微型机' |
| | | if (value === 2 || value === '2') return '植保机' |
| | | return value || '-' |
| | | } |
| | | |
| | | watch( |
| | | () => visibleModel.value, |
| | | (visible) => { |
| | | if (visible) { |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | } |
| | | ) |
| | | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-09 15:06:02 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 15:10:44 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\EmptyState.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-09 15:06:02 |
| | | --> |
| | | <template> |
| | | <div class="empty-state"> |
| | | <div class="empty-content"> |
| | | <img :src="noDataBg" alt="暂无数据"> |
| | | <span class="empty-text">暂无数据</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import noDataBg from '@/assets/images/no-data-bg.png' |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .empty-state { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex: 1; |
| | | width: 100%; |
| | | height: 100%; |
| | | min-height: 220px; |
| | | |
| | | .empty-content { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-direction: column; |
| | | text-align: center; |
| | | } |
| | | |
| | | img { |
| | | max-width: 78%; |
| | | max-height: 78%; |
| | | object-fit: contain; |
| | | } |
| | | |
| | | .empty-text { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #9E9EBA; |
| | | line-height: 18px; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-08 09:29:07 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-08 14:49:25 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\EquipmentWarning.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-08 09:29:07 |
| | | --> |
| | | <template> |
| | | <div |
| | | class="equipment-warning" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <EmptyState v-if="!equipmentWarningData.length" /> |
| | | <EquipmentTemplate |
| | | v-else |
| | | v-for="(item, ind) in equipmentWarningData" |
| | | :key="ind" |
| | | :data="item" |
| | | @click="onCardClick" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from 'vue' |
| | | import EquipmentTemplate from './templateComponents/EquipmentTemplate.vue' |
| | | import { deviceSearchApi } from '@/api/dataCockpit' |
| | | import EmptyState from './EmptyState.vue' |
| | | |
| | | const equipmentWarningData = ref([]) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const statusMap = { |
| | | 0: { text: '在线', statusType: 'online' }, |
| | | 1: { text: '离线', statusType: 'offline' }, |
| | | 2: { text: '故障', statusType: 'fault' }, |
| | | 3: { text: '报废', statusType: 'offline' } |
| | | } |
| | | const deviceTypeMap = { |
| | | 1: '便捷侦测箱', |
| | | 2: '反制枪', |
| | | 3: '察打一体' |
| | | } |
| | | |
| | | const formatDeviceItem = (item) => { |
| | | const statusInfo = statusMap[item?.status] || { text: '-', statusType: '' } |
| | | return { |
| | | id: item.id, |
| | | name: item.deviceName || item.deviceModel || '-', |
| | | status: statusInfo.text, |
| | | statusType: statusInfo.statusType, |
| | | type: deviceTypeMap[item.deviceType] || item.deviceType || '-', |
| | | manufacturer: item.manufacturer || '-', |
| | | azimuth: item.azimuth ?? 0, |
| | | pitch: item.elevation ?? 0, |
| | | range: item.effectiveRangeKm ?? 0, |
| | | model: item.deviceModel || '-', |
| | | longitude: item.longitude ?? '-', |
| | | latitude: item.latitude ?? '-' |
| | | } |
| | | } |
| | | |
| | | const fetchDeviceWarning = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const params = { current: 1, size: 20, deviceStatusList: '1,2,3' } |
| | | const res = await deviceSearchApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | equipmentWarningData.value = records.map(formatDeviceItem) |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchDeviceWarning() |
| | | }) |
| | | |
| | | const onCardClick = (item) => { |
| | | console.log('点击设备卡片:', item.id) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .equipment-warning { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div |
| | | class="history-warning" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <!-- 搜索区(设计稿红框) --> |
| | | <div class="search-box"> |
| | | <!-- 关键字 --> |
| | | <el-input v-model="query.keyword" class="ztzf-data-cockpit-search-input" placeholder="请输入数据名称" clearable |
| | | @keyup.enter="onSearch"> |
| | | </el-input> |
| | | |
| | | <!-- 日期范围 --> |
| | | <el-date-picker v-model="query.dateRange" |
| | | class="ztzf-data-cockpit-date-picker" |
| | | popper-class="ztzf-data-cockpit-date-picker-popper" |
| | | type="daterange" |
| | | range-separator="~" |
| | | start-placeholder="创建开始日期" end-placeholder="结束日期" format="YYYY年MM月DD日" value-format="YYYY-MM-DD" |
| | | :clearable="true" @change="onSearch"> |
| | | </el-date-picker> |
| | | </div> |
| | | |
| | | <!-- 列表 --> |
| | | <div class="list-content"> |
| | | <EmptyState v-if="!filteredList.length" /> |
| | | <HistoryTemplate |
| | | v-else |
| | | v-for="(item, ind) in filteredList" |
| | | :key="item.id || ind" |
| | | :data="item" |
| | | @click="onCardClick" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from 'vue' |
| | | import HistoryTemplate from './templateComponents/HistoryTemplate.vue' |
| | | import { alarmLogApi } from '@/api/dataCockpit' |
| | | import EmptyState from './EmptyState.vue' |
| | | |
| | | const historyWarningData = ref([]) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | // 查询条件:keyword + dateRange(YYYY-MM-DD数组) |
| | | const query = ref({ |
| | | keyword: '', |
| | | dateRange: [] // [start, end] |
| | | }) |
| | | |
| | | const onSearch = () => { |
| | | fetchHistoryWarning() |
| | | } |
| | | |
| | | const onCardClick = (item) => { |
| | | console.log('点击无人机卡片:', item.id) |
| | | } |
| | | |
| | | const fetchHistoryWarning = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const [startTime, endTime] = query.value.dateRange || [] |
| | | const keyword = (query.value.keyword || '').trim() |
| | | const params = { |
| | | current: 1, |
| | | size: 20, |
| | | alarmType: '2', |
| | | startTime, |
| | | endTime, |
| | | droneName: keyword || undefined, |
| | | droneSerialNo: keyword || undefined |
| | | } |
| | | const res = await alarmLogApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | historyWarningData.value = records |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | const filteredList = computed(() => { |
| | | return historyWarningData.value |
| | | }) |
| | | |
| | | onMounted(() => { |
| | | fetchHistoryWarning() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .history-warning { |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 100%; |
| | | height: 100%; |
| | | |
| | | .search-box { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | |
| | | ::v-deep(.ztzf-data-cockpit-date-picker) { |
| | | width: 100% !important; |
| | | } |
| | | } |
| | | |
| | | .list-content { |
| | | height: 0; |
| | | flex: 1; |
| | | } |
| | | } |
| | | |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="left-container" :class="{ collapsed: collapsedModel }"> |
| | | <div class="wrapper"> |
| | | <TitleTemplate>异常情况告警</TitleTemplate> |
| | | |
| | | <div class="category-container"> |
| | | <div class="category-item" v-for="(item, ind) in categoryList" :key="ind" |
| | | @click="activeIdModel = item.id" :class="{ active: activeIdModel === item.id }"> |
| | | {{ item.name }} |
| | | <span class="badge" v-if="item.badge">{{ item.badge }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <component :is="currentComponent" /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="collapse-btn" @click="toggleCollapse"> |
| | | <span class="arrow" :class="collapsedModel ? 'right' : 'left'"></span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import TitleTemplate from './templateComponents/TitleTemplate.vue' |
| | | import RealWarning from './RealWarning.vue' |
| | | import EquipmentWarning from './EquipmentWarning.vue' |
| | | import HistoryWarning from './HistoryWarning.vue' |
| | | import { alarmStatisticsApi } from '@/api/dataCockpit' |
| | | import { onMounted } from 'vue' |
| | | |
| | | const props = defineProps({ |
| | | activeId: { |
| | | type: Number, |
| | | default: 1 |
| | | }, |
| | | collapsed: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:activeId', 'update:collapsed']) |
| | | |
| | | const activeIdModel = computed({ |
| | | get: () => props.activeId, |
| | | set: (val) => emit('update:activeId', val) |
| | | }) |
| | | |
| | | const collapsedModel = computed({ |
| | | get: () => props.collapsed, |
| | | set: (val) => emit('update:collapsed', val) |
| | | }) |
| | | |
| | | const categoryList = ref([ |
| | | { |
| | | id: 1, |
| | | name: '实时告警', |
| | | badge: 0, |
| | | component: RealWarning |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '设备告警', |
| | | badge: 0, |
| | | component: EquipmentWarning |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: '历史告警', |
| | | badge: 0, |
| | | component: HistoryWarning |
| | | } |
| | | ]) |
| | | |
| | | const currentComponent = computed(() => { |
| | | return categoryList.value.find(i => i.id === activeIdModel.value)?.component |
| | | }) |
| | | |
| | | const toggleCollapse = () => { |
| | | collapsedModel.value = !collapsedModel.value |
| | | } |
| | | |
| | | const updateBadges = async () => { |
| | | const res = await alarmStatisticsApi() |
| | | const data = res?.data?.data || {} |
| | | const realTotal = data.realTimeAlarmCount ?? 0 |
| | | const historyTotal = data.historyAlarmCount ?? 0 |
| | | const deviceTotal = data.deviceAlarmCount ?? 0 |
| | | categoryList.value = categoryList.value.map((item) => { |
| | | if (item.id === 1) return { ...item, badge: realTotal } |
| | | if (item.id === 2) return { ...item, badge: deviceTotal } |
| | | if (item.id === 3) return { ...item, badge: historyTotal } |
| | | return item |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | updateBadges() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .left-container { |
| | | position: relative; |
| | | transition: transform 0.3s ease-in-out; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-100%); |
| | | } |
| | | } |
| | | |
| | | .collapse-btn { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 357px; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #fff; |
| | | z-index: 10; |
| | | transform: translateY(-50%); |
| | | |
| | | .arrow { |
| | | width: 24px; |
| | | height: 24px; |
| | | background: url('@/assets/images/dataCockpit/left-btn.png') center / 100% 100% no-repeat; |
| | | } |
| | | |
| | | &:hover { |
| | | background: rgba(40, 79, 227, 0.6); |
| | | } |
| | | } |
| | | |
| | | .category-container { |
| | | position: relative; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; |
| | | bottom: 0; |
| | | width: 100%; |
| | | height: 1px; |
| | | background: #333355; |
| | | } |
| | | |
| | | .category-item { |
| | | position: relative; |
| | | width: 0; |
| | | flex: 1; |
| | | margin-left: 26px; |
| | | height: 68px; |
| | | line-height: 68px; |
| | | |
| | | font-family: Source Code Pro, Source Code Pro; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #86909C; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | cursor: pointer; |
| | | |
| | | &:first-child { |
| | | margin-left: 0; |
| | | } |
| | | |
| | | &.active { |
| | | font-weight: normal; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; |
| | | bottom: 0; |
| | | width: 100%; |
| | | height: 0; |
| | | } |
| | | |
| | | &.active::after { |
| | | height: 3px; |
| | | background: #284FE3; |
| | | box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36); |
| | | border-radius: 5px 5px 0px 0px; |
| | | z-index: 9; |
| | | } |
| | | |
| | | .badge { |
| | | position: absolute; |
| | | top: 16px; |
| | | right: 4px; |
| | | min-width: 18px; |
| | | line-height: 18px; |
| | | font-size: 12px; |
| | | color: #FFFFFF; |
| | | background: #FF3B30; |
| | | border-radius: 50%; |
| | | text-align: center; |
| | | box-shadow: 0px 2px 6px rgba(255, 59, 48, 0.6); |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | height: 0; |
| | | flex: 1; |
| | | margin-top: 20px; |
| | | overflow-x: hidden; |
| | | overflow-y: auto; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-08 09:29:07 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-08 14:49:09 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\RealWarning.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-08 09:29:07 |
| | | --> |
| | | <template> |
| | | <div |
| | | class="real-warning" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <EmptyState v-if="!realWarningData.length" /> |
| | | <RealTemplate |
| | | v-else |
| | | v-for="(item, ind) in realWarningData" |
| | | :key="ind" |
| | | :data="item" |
| | | @signal="() => handleAction('signal', item)" |
| | | @counter="() => handleAction('counter', item)" |
| | | @favorite="() => console.log('收藏', item.id)" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import RealTemplate from './templateComponents/RealTemplate.vue' |
| | | import { alarmLogApi, interferenceAndExpulsionApi } from '@/api/dataCockpit' |
| | | import { onMounted, ref } from 'vue' |
| | | import EmptyState from './EmptyState.vue' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | | const realWarningData = ref([]) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const actionTextMap = { |
| | | signal: '信号干扰', |
| | | counter: '诱导驱离' |
| | | } |
| | | |
| | | const buildPayload = (item) => ({ |
| | | alarmRecordId: item.id, |
| | | areaCode: item.areaCode, |
| | | counterEffect: '', |
| | | coverRadiusM: item.coverRadiusM, |
| | | deployLatitude: item.latitude, |
| | | deployLongitude: item.longitude, |
| | | deviceCode: item.deviceCode, |
| | | deviceId: item.deviceId, |
| | | deviceModel: item.deviceModel, |
| | | deviceName: item.deviceName, |
| | | deviceType: item.deviceType, |
| | | droneDeviceCode: item.droneSerialNo, |
| | | droneName: item.droneName, |
| | | droneType: item.droneType, |
| | | findTime: item.alarmTime, |
| | | id: item.id, |
| | | workMode: '' |
| | | }) |
| | | |
| | | const handleAction = async (type, item) => { |
| | | const actionText = actionTextMap[type] || '操作' |
| | | try { |
| | | const res = await interferenceAndExpulsionApi(buildPayload(item)) |
| | | if (res?.data?.success) { |
| | | ElMessage({ type: 'success', message: `${actionText}成功` }) |
| | | } else { |
| | | ElMessage({ type: 'error', message: res?.data?.msg || `${actionText}失败` }) |
| | | } |
| | | } catch (error) { |
| | | ElMessage({ type: 'error', message: `${actionText}失败` }) |
| | | } |
| | | } |
| | | |
| | | const fetchRealWarning = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const params = { current: 1, size: 20, alarmType: '1' } |
| | | const res = await alarmLogApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | realWarningData.value = records |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchRealWarning() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .real-warning { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-07 15:17:54 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 16:54:31 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\RightContainer.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-07 15:17:54 |
| | | --> |
| | | <template> |
| | | <div class="right-container" :class="{ collapsed: collapsedModel }"> |
| | | <div class="wrapper"> |
| | | <TitleTemplate>侦测反制设备</TitleTemplate> |
| | | |
| | | <div class="category-container"> |
| | | <div class="category-item" v-for="(item, ind) in categoryList" :key="ind"> |
| | | <div class="category-item-logo"> |
| | | <img :src="item.logo" alt=""> |
| | | </div> |
| | | |
| | | <div class="category-item-content"> |
| | | <span class="name">{{ item.name }}</span> |
| | | <span class="val">{{ item.val }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | class="content" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <EmptyState v-if="!counterDeviceStatusList.length" /> |
| | | <RealEquipmentTemplate |
| | | v-else |
| | | v-for="item in counterDeviceStatusList" |
| | | :key="item.id" |
| | | :data="item" |
| | | @history="onHistory" |
| | | @click="onCardClick" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="collapse-btn" @click="toggleCollapse"> |
| | | <span class="arrow" :class="collapsedModel ? 'left' : 'right'"></span> |
| | | </div> |
| | | |
| | | <DeviceHistoryDialog v-model="historyVisible" :device="historyDevice" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import TitleTemplate from './templateComponents/TitleTemplate.vue' |
| | | import RealEquipmentTemplate from './templateComponents/RealEquipmentTemplate.vue' |
| | | import DeviceHistoryDialog from './DeviceHistoryDialog.vue' |
| | | import EmptyState from './EmptyState.vue' |
| | | import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png' |
| | | import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png' |
| | | import { deviceSearchApi, deviceStatisticsApi } from '@/api/dataCockpit' |
| | | |
| | | const props = defineProps({ |
| | | collapsed: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:collapsed', 'update:onlineDevices']) |
| | | |
| | | const collapsedModel = computed({ |
| | | get: () => props.collapsed, |
| | | set: (val) => emit('update:collapsed', val) |
| | | }) |
| | | |
| | | const categoryList = ref([ |
| | | { |
| | | id: 1, |
| | | name: '侦测设备', |
| | | val: 0, |
| | | logo: zcsbLogo |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '反制设备', |
| | | val: 0, |
| | | logo: fzsbLogo |
| | | } |
| | | ]) |
| | | |
| | | const counterDeviceStatusList = ref([]) |
| | | |
| | | const historyVisible = ref(false) |
| | | const historyDevice = ref(null) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const onHistory = (item) => { |
| | | historyDevice.value = item |
| | | historyVisible.value = true |
| | | console.log('历史数据:', item.id) |
| | | } |
| | | |
| | | const onCardClick = (item) => { |
| | | console.log('点击卡片:', item.id) |
| | | } |
| | | const toggleCollapse = () => { |
| | | collapsedModel.value = !collapsedModel.value |
| | | } |
| | | |
| | | const statusMap = { |
| | | 0: { text: '在线', actionType: 'detecting' }, |
| | | 1: { text: '离线', actionType: '' }, |
| | | 2: { text: '故障', actionType: 'jamming' }, |
| | | 3: { text: '报废', actionType: '' } |
| | | } |
| | | const deviceTypeMap = { |
| | | 1: '便捷侦测箱', |
| | | 2: '反制枪', |
| | | 3: '察打一体' |
| | | } |
| | | |
| | | const formatDeviceItem = (item) => { |
| | | const statusInfo = statusMap[item?.status] || { text: '-', actionType: '' } |
| | | return { |
| | | id: item.id, |
| | | name: item.deviceName || item.deviceModel || '-', |
| | | actionText: statusInfo.text, |
| | | actionType: statusInfo.actionType, |
| | | type: deviceTypeMap[item.deviceType] || item.deviceType || '-', |
| | | batteryPct: item.batteryPct ?? 0, |
| | | azimuth: item.azimuth ?? 0, |
| | | elevation: item.elevation ?? 0, |
| | | status: item.status ?? '-', |
| | | manufacturer: item.manufacturer || '-', |
| | | detectTargetCnt: item.detectTargetCnt ?? 0, |
| | | effectiveRangeKm: item.effectiveRangeKm ?? 0 |
| | | } |
| | | } |
| | | |
| | | const fetchDeviceList = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const params = { current: 1, size: 20, deviceStatusList: '0' } |
| | | const res = await deviceSearchApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | counterDeviceStatusList.value = records.map(formatDeviceItem) |
| | | emit('update:onlineDevices', records) |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchDeviceList() |
| | | fetchDeviceStatistics() |
| | | }) |
| | | |
| | | const normalizeType = (value) => { |
| | | if (value === 1 || value === '1') return '侦测设备' |
| | | if (value === 2 || value === '2') return '反制设备' |
| | | if (value === 'detect') return '侦测设备' |
| | | if (value === 'counter') return '反制设备' |
| | | return value |
| | | } |
| | | |
| | | const fetchDeviceStatistics = async () => { |
| | | const res = await deviceStatisticsApi() |
| | | const list = res?.data?.data ?? [] |
| | | const map = new Map(list.map((item) => [normalizeType(item.type), item.count])) |
| | | categoryList.value = categoryList.value.map((item) => ({ |
| | | ...item, |
| | | val: map.has(item.name) ? map.get(item.name) : item.val |
| | | })) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .category-container { |
| | | margin-top: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 28px; |
| | | box-sizing: border-box; |
| | | |
| | | .category-item { |
| | | position: relative; |
| | | width: 0; |
| | | flex: 1; |
| | | display: flex; |
| | | height: 64px; |
| | | |
| | | &-logo { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | img { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | } |
| | | |
| | | &-content { |
| | | width: 0; |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | border-bottom: 1px solid #6C6C89; |
| | | |
| | | span { |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .name { |
| | | font-family: Open Sans, Open Sans; |
| | | font-weight: 400; |
| | | font-size: 16px; |
| | | color: #C3C3DD; |
| | | line-height: 20px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .val { |
| | | margin-top: 10px; |
| | | font-family: DIN, DIN; |
| | | font-weight: bold; |
| | | font-size: 22px; |
| | | color: #FFFFFF; |
| | | line-height: 20px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | height: 0; |
| | | flex: 1; |
| | | margin-top: 20px; |
| | | overflow-x: hidden; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | |
| | | .right-container { |
| | | transition: transform 0.3s ease-in-out; |
| | | position: relative; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(100%); |
| | | } |
| | | } |
| | | |
| | | .collapse-btn { |
| | | position: absolute; |
| | | top: 50%; |
| | | right: 357px; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #fff; |
| | | z-index: 10; |
| | | transform: translateY(-50%); |
| | | |
| | | .arrow { |
| | | width: 24px; |
| | | height: 24px; |
| | | background: url('@/assets/images/dataCockpit/right-btn.png') center / 100% 100% no-repeat; |
| | | } |
| | | |
| | | &:hover { |
| | | background: rgba(40, 79, 227, 0.6); |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="device-card" @click="emit('click', data)"> |
| | | <div class="header"> |
| | | <div class="title"> |
| | | <span class="name">{{ data.name }}</span> |
| | | |
| | | <span class="status"> |
| | | <span :class="data.statusType"> |
| | | |
| | | </span> |
| | | |
| | | <span> |
| | | {{ data.status }} |
| | | </span> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">类型</span> |
| | | {{ data.type }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">厂商</span> |
| | | {{ data.manufacturer }} |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">方位角</span> |
| | | {{ data.azimuth }}° |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">俯仰角</span> |
| | | {{ data.pitch }}° |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">有效范围</span> |
| | | {{ data.range }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">设备型号</span> |
| | | {{ data.model }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div> |
| | | <span class="label">部署位置</span> |
| | | <span class="value">{{ data.longitude }},{{ data.latitude }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | const props = defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['click']) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .device-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .status { |
| | | padding: 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 21px; |
| | | |
| | | background: #303041; |
| | | border-radius: 10px; |
| | | gap: 6px; |
| | | |
| | | span { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 10px; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | |
| | | span.offline { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #939393; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | span.fault { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #FF4444; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | &>div { |
| | | margin-top: 10px; |
| | | line-height: 22px; |
| | | |
| | | span.label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | |
| | | .row { |
| | | display: flex; |
| | | |
| | | &>div { |
| | | width: 0; |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="drone-info-card" @click="emit('click', data)"> |
| | | <div class="title"> |
| | | <span class="name"> |
| | | {{ data.droneName }} |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="rows"> |
| | | <div class="row"> |
| | | <span class="label">序列号</span> |
| | | <span class="value">{{ data.droneSerialNo }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">数据源</span> |
| | | <span class="value">{{ getDataSource(data) }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">信号频段</span> |
| | | <span class="value">{{ formatFrequency(data.signalFreqMhz) }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">发现时间</span> |
| | | <span class="value"> |
| | | <span class="date">{{ getAlarmDate(data.alarmTime) }}</span> |
| | | <span class="time">{{ getAlarmTime(data.alarmTime) }}</span> |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">停留时间</span> |
| | | <span class="value">{{ data.stayDuration }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">反制方式</span> |
| | | <span class="value">{{ getCounterWayText(data.counterWay) }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">飞行状态</span> |
| | | <span class="value">{{ getFlightStatusText(data.flightStatus) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['click']) |
| | | |
| | | const getDataSource = (item) => { |
| | | return item?.deviceName || item?.areaName || '-' |
| | | } |
| | | |
| | | const formatFrequency = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}MHZ` |
| | | } |
| | | |
| | | const getAlarmDate = (value) => { |
| | | if (!value) return '-' |
| | | const parts = value.replace('T', ' ').split(' ') |
| | | return parts[0] || value |
| | | } |
| | | |
| | | const getAlarmTime = (value) => { |
| | | if (!value) return '-' |
| | | const parts = value.replace('T', ' ').split(' ') |
| | | return parts[1] || value |
| | | } |
| | | |
| | | const getFlightStatusText = (value) => { |
| | | if (value === 1 || value === '1') return '侦测中' |
| | | if (value === 2 || value === '2') return '反制中' |
| | | return value || '-' |
| | | } |
| | | |
| | | const getCounterWayText = (value) => { |
| | | if (value === 1 || value === '1') return '信号干扰' |
| | | if (value === 2 || value === '2') return '诱导驱离' |
| | | if (value === 3 || value === '3') return '无' |
| | | return value || '-' |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .drone-info-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:hover { |
| | | filter: brightness(1.05); |
| | | } |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | |
| | | .rows { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | .row { |
| | | line-height: 22px; |
| | | |
| | | .label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="counter-card" @click="emit('click', data)"> |
| | | <!-- 头部 --> |
| | | <div class="header"> |
| | | <div class="title"> |
| | | <span class="name">{{ data.name }}</span> |
| | | |
| | | <span class="status"> |
| | | <span :class="getStatusType(data.status)"> |
| | | |
| | | </span> |
| | | |
| | | <span> |
| | | {{ getStatusText(data.status) }} |
| | | </span> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 字段区:两列 --> |
| | | <div class="content"> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">类型</span> |
| | | {{ data.type }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">电量</span> |
| | | {{ data.batteryPct }}% |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">方位角</span> |
| | | {{ data.azimuth }}° |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">俯仰角</span> |
| | | {{ data.elevation }}° |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">状态</span> |
| | | {{ getStatusText(data.status) }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">厂商</span> |
| | | {{ data.manufacturer }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">侦测目标</span> |
| | | {{ data.detectTargetCnt }}台 |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">有效范围</span> |
| | | {{ data.effectiveRangeKm }}KM |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 底部按钮 --> |
| | | <div class="footer"> |
| | | <el-button color="#284FE3" type="primary" @click.stop="emit('history', data)">历史数据</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['history', 'click']) |
| | | |
| | | const getStatusText = (value) => { |
| | | if (value === 0 || value === '0') return '在线' |
| | | if (value === 1 || value === '1') return '离线' |
| | | if (value === 2 || value === '2') return '故障' |
| | | if (value === 3 || value === '3') return '报废' |
| | | return value || '-' |
| | | } |
| | | |
| | | const getStatusType = (value) => { |
| | | if (value === 0 || value === '0') return 'detecting' |
| | | if (value === 2 || value === '2') return 'jamming' |
| | | return '' |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .counter-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .status { |
| | | padding: 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 21px; |
| | | |
| | | background: #303041; |
| | | border-radius: 10px; |
| | | gap: 6px; |
| | | |
| | | span { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 10px; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | |
| | | span.detecting { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #18FF4A; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | span.jamming { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #FF4444; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | &>div { |
| | | margin-top: 10px; |
| | | line-height: 22px; |
| | | |
| | | span.label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | |
| | | .row { |
| | | display: flex; |
| | | |
| | | &>div { |
| | | width: 0; |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | margin-top: 10px; |
| | | |
| | | ::v-deep(.el-button) { |
| | | width: 96px; |
| | | height: 36px; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="drone-card"> |
| | | <div class="header"> |
| | | <div class="title"> |
| | | <span class="name">{{ data.droneName }}</span> |
| | | <span class="status"> |
| | | <span :class="getStatusType(data)"> |
| | | |
| | | </span> |
| | | |
| | | <span> |
| | | {{ getStatusText(data) }} |
| | | </span> |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="actions"> |
| | | <span class="star" @click="emit('favorite')"> |
| | | {{ data.isFavorite ? '★' : '☆' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <div> |
| | | <span class="label">序列号</span> |
| | | {{ data.droneSerialNo }} |
| | | </div> |
| | | <div> |
| | | <span class="label">数据源</span> |
| | | {{ getDataSource(data) }} |
| | | </div> |
| | | <div> |
| | | <span class="label">经纬度</span> |
| | | {{ data.longitude }}, {{ data.latitude }} |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">信号频段</span> |
| | | {{ formatFrequency(data.signalFreqMhz) }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">发现时间</span> |
| | | {{ getAlarmTime(data.alarmTime) }} |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">飞行高度</span> |
| | | {{ formatHeight(data.flightHeightM) }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">飞行速度</span> |
| | | {{ formatSpeed(data.flightSpeedMs) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="footer"> |
| | | <el-button class="general" color="#2B2B4C" @click="emit('signal')">信号干扰</el-button> |
| | | <el-button color="#284FE3" type="primary" @click="emit('counter')">诱导驱离</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | const props = defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['signal', 'counter', 'favorite']) |
| | | |
| | | const getStatusText = (item) => { |
| | | const status = item?.flightStatus |
| | | if (status === 1 || status === '1') return '侦测中' |
| | | if (status === 2 || status === '2') return '反制中' |
| | | return status || '侦测中' |
| | | } |
| | | |
| | | const getStatusType = (item) => { |
| | | const statusText = getStatusText(item) |
| | | const counterWay = item?.counterWay |
| | | if (statusText.includes('反制') || counterWay === 2 || counterWay === '2' || counterWay === '诱导驱离') { |
| | | return 'countering' |
| | | } |
| | | return 'detecting' |
| | | } |
| | | |
| | | const getDataSource = (item) => { |
| | | return item?.deviceName || item?.areaName || '-' |
| | | } |
| | | |
| | | const formatFrequency = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}MHz` |
| | | } |
| | | |
| | | const getAlarmTime = (value) => { |
| | | if (!value) return '-' |
| | | const parts = value.replace('T', ' ').split(' ') |
| | | return parts[1] || value |
| | | } |
| | | |
| | | const formatHeight = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}m` |
| | | } |
| | | |
| | | const formatSpeed = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}m/s` |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .drone-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:hover { |
| | | background: #29294D; |
| | | ; |
| | | } |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .status { |
| | | padding: 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 21px; |
| | | |
| | | background: #303041; |
| | | border-radius: 10px; |
| | | gap: 6px; |
| | | |
| | | span { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 10px; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | |
| | | span.detecting { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #18FF4A; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | span.countering { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #FF4444; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | font-size: 16px; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | &>div { |
| | | margin-top: 10px; |
| | | line-height: 22px; |
| | | |
| | | span.label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | |
| | | .row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | } |
| | | |
| | | .footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | margin-top: 10px; |
| | | |
| | | ::v-deep(.el-button) { |
| | | width: 96px; |
| | | height: 36px; |
| | | |
| | | &.general.el-button { |
| | | border-radius: 4px 4px 4px 4px; |
| | | border: 1px solid rgba(255, 255, 255, 0.5); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-08 09:32:52 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-08 09:43:00 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\templateComponents\TitleTemplate.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-08 09:32:52 |
| | | --> |
| | | <template> |
| | | <div class="title-box"> |
| | | <div class="title"> |
| | | <slot></slot> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .title-box { |
| | | padding-left: 26px; |
| | | background: url('@/assets/images/dataCockpit/title-bg.png') center / 100% 100% no-repeat; |
| | | height: 34px; |
| | | |
| | | .title { |
| | | height: 25px; |
| | | line-height: 25px; |
| | | |
| | | font-family: PangMen; |
| | | font-weight: 400; |
| | | font-size: 18px; |
| | | color: #FFFFFF; |
| | | letter-spacing: 1px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-06 16:35:50 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 16:34:39 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\index.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-06 16:35:50 |
| | | --> |
| | | <template> |
| | | <div class="page-container"> |
| | | <DeviceMapContainer |
| | | :online-devices="onlineDevices" |
| | | :left-collapsed="leftCollapsed" |
| | | container-id="data-cockpit-map" |
| | | /> |
| | | <LeftContainer class="left-container" v-model:activeId="leftActiveId" v-model:collapsed="leftCollapsed" /> |
| | | <RightContainer class="right-container" v-model:collapsed="rightCollapsed" v-model:onlineDevices="onlineDevices" /> |
| | | <CenterContainer @select-warning="onSelectWarning" @select-equipment="onSelectEquipment" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import DeviceMapContainer from '@/components/map-container/device-map-container.vue'; |
| | | import LeftContainer from './components/LeftContainer.vue'; |
| | | import RightContainer from './components/RightContainer.vue'; |
| | | import CenterContainer from './components/CenterContainer.vue'; |
| | | |
| | | const leftActiveId = ref(1); |
| | | const leftCollapsed = ref(false); |
| | | |
| | | const rightCollapsed = ref(false); |
| | | const onlineDevices = ref([]); |
| | | |
| | | const onSelectWarning = (type) => { |
| | | leftActiveId.value = type; |
| | | leftCollapsed.value = false; |
| | | }; |
| | | |
| | | const onSelectEquipment = () => { |
| | | rightCollapsed.value = false; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .page-container { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .left-container, |
| | | .right-container { |
| | | position: absolute; |
| | | top: 96px; |
| | | bottom: 0; |
| | | width: 401px; |
| | | z-index: 9; |
| | | backdrop-filter: blur(2px); |
| | | |
| | | ::v-deep(.wrapper) { |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: absolute; |
| | | top: 17px; |
| | | bottom: 20px; |
| | | padding: 20px; |
| | | width: 300px; |
| | | background: rgba(0,0,0,0.3); |
| | | backdrop-filter: blur(16px); |
| | | border-radius: 10px 10px 10px 10px; |
| | | } |
| | | } |
| | | |
| | | .left-container { |
| | | top: 96px; |
| | | left: 0; |
| | | background: linear-gradient( 270deg, rgba(17,23,34,0) 0%, rgba(17,23,34,0.56) 50%, rgba(17,23,34,0.96) 100%); |
| | | |
| | | ::v-deep(.wrapper) { |
| | | left: 17px; |
| | | } |
| | | } |
| | | |
| | | .right-container { |
| | | top: 54px; |
| | | right: 0; |
| | | background: linear-gradient( 270deg, rgba(17,23,34,0.96) 0.16%, rgba(17,23,34,0.56) 57.57%, rgba(4,30,37,0) 100%); |
| | | |
| | | ::v-deep(.wrapper) { |
| | | top: 21px; |
| | | right: 17px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | const viewInstance = shallowRef(null); |
| | | const homeViewer = shallowRef(null); |
| | | let tbJwdList = []; |
| | | const loading = ref(false); |
| | | const loading = ref(true); |
| | | const tableLoading = ref(true); |
| | | const tableData = ref([]); |
| | | const AlltableData = ref([]); |
| | |
| | | label: '新大屏', |
| | | value: 4, |
| | | }, |
| | | { |
| | | label: '无人机管控', |
| | | value: 5, |
| | | }, |
| | | ], |
| | | hide: true, |
| | | rules: [ |
| New file |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false" width="800px"> |
| | | <!-- 查看模式 --> |
| | | <div v-if="readonly" class="view-mode"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">登录账号:</span> |
| | | {{ formData.account }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户姓名:</span> |
| | | {{ formData.realName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">手机号码:</span> |
| | | {{ formData.phone }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">电子邮箱:</span> |
| | | {{ formData.email }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户性别:</span> |
| | | {{ getSexLabel(formData.sex) }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户生日:</span> |
| | | {{ formData.birthday }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">所属角色:</span> |
| | | {{ formData.roleName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">所属部门:</span> |
| | | {{ formData.deptName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户编号:</span> |
| | | {{ formData.code }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">账号状态:</span> |
| | | {{ formData.statusName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">到期时间:</span> |
| | | {{ formData.expireTime }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">次数限制:</span> |
| | | {{ formData.maxLoginNum }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">飞行开始时间:</span> |
| | | {{ formData.flightStartTime }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">飞行结束时间:</span> |
| | | {{ formData.flightEndTime }} |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 编辑/新增模式 --> |
| | | <el-form v-else ref="formRef" :model="formData" :rules="rules" label-width="110px"> |
| | | <el-divider content-position="left">基础信息</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="登录账号" prop="account"> |
| | | <el-input v-model="formData.account" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12" v-if="dialogMode === 'add'"> |
| | | <el-form-item label="密码" prop="password"> |
| | | <el-input |
| | | v-model="formData.password" |
| | | type="password" |
| | | maxlength="50" |
| | | placeholder="请输入" |
| | | clearable |
| | | show-password |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12" v-if="dialogMode === 'add'"> |
| | | <el-form-item label="确认密码" prop="password2"> |
| | | <el-input |
| | | v-model="formData.password2" |
| | | type="password" |
| | | maxlength="50" |
| | | placeholder="请输入" |
| | | clearable |
| | | show-password |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider content-position="left">详细信息</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户姓名" prop="realName"> |
| | | <el-input v-model="formData.realName" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="手机号码" prop="phone"> |
| | | <el-input v-model="formData.phone" maxlength="20" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="电子邮箱" prop="email"> |
| | | <el-input v-model="formData.email" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户性别" prop="sex"> |
| | | <el-select v-model="formData.sex" placeholder="请选择" clearable> |
| | | <el-option label="男" :value="1" /> |
| | | <el-option label="女" :value="2" /> |
| | | <el-option label="未知" :value="3" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户生日" prop="birthday"> |
| | | <el-date-picker |
| | | v-model="formData.birthday" |
| | | type="datetime" |
| | | placeholder="请选择" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider content-position="left">职责信息</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户编号" prop="code"> |
| | | <el-input v-model="formData.code" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="所属角色" prop="roleId"> |
| | | <el-tree-select |
| | | v-model="formData.roleId" |
| | | :data="roleTree" |
| | | :props="treeProps" |
| | | node-key="id" |
| | | check-strictly |
| | | clearable |
| | | placeholder="请选择" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="所属部门" prop="deptId"> |
| | | <el-tree-select |
| | | v-model="formData.deptId" |
| | | :data="deptTree" |
| | | :props="deptTreeSelectProps" |
| | | node-key="id" |
| | | check-strictly |
| | | clearable |
| | | placeholder="请选择" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider content-position="left">登录限制</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="到期时间" prop="expireTime"> |
| | | <el-date-picker |
| | | v-model="formData.expireTime" |
| | | type="datetime" |
| | | placeholder="请选择" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="次数限制" prop="maxLoginNum"> |
| | | <el-input v-model="formData.maxLoginNum" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="飞行开始时间" prop="flightStartTime"> |
| | | <el-time-picker |
| | | v-model="formData.flightStartTime" |
| | | placeholder="请选择" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="飞行结束时间" prop="flightEndTime"> |
| | | <el-time-picker |
| | | v-model="formData.flightEndTime" |
| | | placeholder="请选择" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <el-button @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button> |
| | | <el-button v-if="!readonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit"> |
| | | 确定 |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getUser, add, update } from '@/api/system/user' |
| | | import { getDeptTree } from '@/api/system/dept' |
| | | import { getRoleTree } from '@/api/system/role' |
| | | import func from '@/utils/func' |
| | | |
| | | // 初始化表单 |
| | | const initForm = () => ({ |
| | | account: '', |
| | | password: '', |
| | | password2: '', |
| | | realName: '', |
| | | phone: '', |
| | | email: '', |
| | | sex: null, |
| | | birthday: '', |
| | | code: '', |
| | | roleId: '', |
| | | deptId: '', |
| | | expireTime: '', |
| | | maxLoginNum: '', |
| | | flightStartTime: '', |
| | | flightEndTime: '', |
| | | }) |
| | | |
| | | const treeProps = { |
| | | label: 'title', |
| | | children: 'children', |
| | | } |
| | | const deptTreeSelectProps = { |
| | | label: 'name', |
| | | value: 'id', |
| | | children: 'children', |
| | | } |
| | | |
| | | const emit = defineEmits(['success']) |
| | | const formRef = ref(null) |
| | | const formData = ref(initForm()) |
| | | const visible = defineModel() |
| | | const dialogMode = ref('add') |
| | | const submitting = ref(false) |
| | | const readonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑用户', view: '查看用户', add: '新增用户' }) |
| | | const roleTree = ref([]) |
| | | const deptTree = ref([]) |
| | | |
| | | // 密码验证 |
| | | const validatePass = (rule, value, callback) => { |
| | | if (value === '') { |
| | | callback(new Error('请输入密码')) |
| | | } else { |
| | | callback() |
| | | } |
| | | } |
| | | const validatePass2 = (rule, value, callback) => { |
| | | if (value === '') { |
| | | callback(new Error('请再次输入密码')) |
| | | } else if (value !== formData.value.password) { |
| | | callback(new Error('两次输入密码不一致!')) |
| | | } else { |
| | | callback() |
| | | } |
| | | } |
| | | |
| | | const rules = { |
| | | account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }], |
| | | password: [{ required: true, validator: validatePass, trigger: 'blur' }], |
| | | password2: [{ required: true, validator: validatePass2, trigger: 'blur' }], |
| | | realName: [ |
| | | { required: true, message: '请输入用户姓名', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: '姓名长度在2到50个字符', trigger: 'blur' }, |
| | | ], |
| | | roleId: [{ required: true, message: '请选择所属角色', trigger: 'change' }], |
| | | deptId: [{ required: true, message: '请选择所属部门', trigger: 'change' }], |
| | | } |
| | | |
| | | // 获取性别标签 |
| | | function getSexLabel(sex) { |
| | | const map = { 1: '男', 2: '女', 3: '未知' } |
| | | return map[sex] || '' |
| | | } |
| | | |
| | | // 获取角色树 |
| | | async function getRoleTreeData() { |
| | | const res = await getRoleTree('000000') |
| | | roleTree.value = res.data.data |
| | | } |
| | | |
| | | // 获取部门树 |
| | | async function getDeptTreeData() { |
| | | const res = await getDeptTree('000000') |
| | | deptTree.value = res.data.data |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 验证飞行时间 |
| | | function validateFlightTime() { |
| | | if (formData.value.flightStartTime && formData.value.flightEndTime) { |
| | | const [startHours, startMinutes] = formData.value.flightStartTime.split(':').map(Number) |
| | | const [endHours, endMinutes] = formData.value.flightEndTime.split(':').map(Number) |
| | | const startTotalMinutes = startHours * 60 + startMinutes |
| | | const endTotalMinutes = endHours * 60 + endMinutes |
| | | if (endTotalMinutes < startTotalMinutes) { |
| | | ElMessage.error('飞行结束时间不能小于飞行开始时间') |
| | | return false |
| | | } |
| | | } |
| | | return true |
| | | } |
| | | |
| | | // 提交新增/编辑 |
| | | async function handleSubmit() { |
| | | const isValid = await formRef.value?.validate().catch(() => false) |
| | | if (!isValid) return |
| | | if (!validateFlightTime()) return |
| | | |
| | | submitting.value = true |
| | | try { |
| | | const submitData = { ...formData.value } |
| | | submitData.roleId = func.toStr(submitData.roleId) |
| | | submitData.name = submitData.realName |
| | | |
| | | if (dialogMode.value === 'add') { |
| | | await add(submitData) |
| | | ElMessage.success('新增成功') |
| | | } else { |
| | | await update(submitData) |
| | | ElMessage.success('更新成功') |
| | | } |
| | | visible.value = false |
| | | emit('success') |
| | | } finally { |
| | | submitting.value = false |
| | | } |
| | | } |
| | | |
| | | // 加载详情 |
| | | async function loadDetail() { |
| | | if (!formData.value.id) return |
| | | const res = await getUser(formData.value.id) |
| | | const data = res?.data?.data ?? {} |
| | | // 处理 roleId |
| | | if (data.roleId) { |
| | | data.roleId = func.split(data.roleId)[0] || data.roleId |
| | | } |
| | | formData.value = data |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | if (dialogMode.value === 'add') { |
| | | formData.value = initForm() |
| | | } else { |
| | | formData.value = { id: row.id } |
| | | await loadDetail() |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getRoleTreeData() |
| | | getDeptTreeData() |
| | | }) |
| | | |
| | | defineExpose({ open }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .view-mode { |
| | | .view-item { |
| | | padding: 10px 0; |
| | | .label { |
| | | color: #606266; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-date-editor) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-tree-select) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <basic-container> |
| | | <!-- 左侧部门树 --> |
| | | <el-row :gutter="16" class="user-container"> |
| | | <el-col :span="4"> |
| | | <div class="tree-box"> |
| | | <el-scrollbar> |
| | | <el-tree |
| | | :data="deptTree" |
| | | :props="deptTreeProps" |
| | | node-key="id" |
| | | :load="loadDeptTree" |
| | | lazy |
| | | @node-click="handleDeptNodeClick" |
| | | /> |
| | | </el-scrollbar> |
| | | </div> |
| | | </el-col> |
| | | <!-- 右侧列表区 --> |
| | | <el-col :span="20"> |
| | | <!-- 搜索表单 --> |
| | | <el-form ref="queryParamsRef" :model="searchParams"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="5"> |
| | | <el-form-item label="登录账号" prop="account"> |
| | | <el-input v-model="searchParams.account" placeholder="请输入" clearable @clear="handleSearch" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="用户姓名" prop="realName"> |
| | | <el-input v-model="searchParams.realName" placeholder="请输入" clearable @clear="handleSearch" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-button @click="resetForm">重置</el-button> |
| | | <el-button type="primary" @click="handleSearch">查询</el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <div class="btn-group"> |
| | | <el-button v-if="permission.user_add && !auditMode" type="primary" @click="openForm('add')">新增</el-button> |
| | | <el-button |
| | | v-if="permission.user_delete && !auditMode" |
| | | type="danger" |
| | | :disabled="!selectedIds.length" |
| | | @click="handleDelete()" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && auditMode" |
| | | type="success" |
| | | :disabled="!selectedIds.length" |
| | | @click="handleAuditPass" |
| | | > |
| | | 通过 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && auditMode" |
| | | type="danger" |
| | | :disabled="!selectedIds.length" |
| | | @click="handleAuditRefuse" |
| | | > |
| | | 拒绝 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && auditMode" |
| | | type="primary" |
| | | plain |
| | | @click="handleAuditBack" |
| | | > |
| | | 返回 |
| | | </el-button> |
| | | <el-button |
| | | v-if="permission.user_role && !auditMode" |
| | | type="info" |
| | | plain |
| | | :disabled="!selectedIds.length" |
| | | @click="handleGrant" |
| | | > |
| | | 角色配置 |
| | | </el-button> |
| | | <el-button |
| | | v-if="permission.user_reset && !auditMode" |
| | | type="info" |
| | | plain |
| | | :disabled="!selectedIds.length" |
| | | @click="handleReset" |
| | | > |
| | | 密码重置 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && !auditMode" |
| | | type="info" |
| | | plain |
| | | :disabled="!selectedIds.length" |
| | | @click="handleLock" |
| | | > |
| | | 账号解封 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && !auditMode" |
| | | type="success" |
| | | plain |
| | | @click="handleImport" |
| | | > |
| | | 导入 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && !auditMode" |
| | | type="warning" |
| | | plain |
| | | @click="handleExport" |
| | | > |
| | | 导出 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 用户列表 --> |
| | | <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column type="index" width="60" label="序号" /> |
| | | <el-table-column prop="account" label="登录账号" /> |
| | | <el-table-column prop="realName" label="用户姓名" /> |
| | | <el-table-column prop="roleName" label="所属角色"> |
| | | <template v-slot="{ row }"> |
| | | <el-tag v-if="row.roleName">{{ row.roleName }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deptName" label="所属部门"> |
| | | <template v-slot="{ row }"> |
| | | <el-tag v-if="row.deptName">{{ row.deptName }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="phone" label="手机号码" /> |
| | | <el-table-column label="操作" width="180"> |
| | | <template v-slot="{ row }"> |
| | | <el-link v-if="permission.user_view" @click="openForm('view', row)" type="primary">查看</el-link> |
| | | <el-link v-if="permission.user_edit && !auditMode" @click="openForm('edit', row)" type="warning"> |
| | | 编辑 |
| | | </el-link> |
| | | <el-link v-if="permission.user_delete && !auditMode" @click="handleDelete(row)" type="danger"> |
| | | 删除 |
| | | </el-link> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- 分页 --> |
| | | <div> |
| | | <el-pagination |
| | | v-model:current-page="searchParams.current" |
| | | v-model:page-size="searchParams.size" |
| | | :total="total" |
| | | @change="getList" |
| | | /> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 用户表单弹框 --> |
| | | <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" @success="getList" /> |
| | | |
| | | <!-- 角色配置弹框 --> |
| | | <el-dialog title="用户角色配置" append-to-body v-model="roleBox" width="345px"> |
| | | <el-tree |
| | | :data="roleGrantList" |
| | | show-checkbox |
| | | check-strictly |
| | | default-expand-all |
| | | node-key="id" |
| | | ref="treeRoleRef" |
| | | :default-checked-keys="roleTreeObj" |
| | | :props="roleProps" |
| | | ></el-tree> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="roleBox = false">取 消</el-button> |
| | | <el-button type="primary" @click="submitRole">确 定</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 导入弹框 --> |
| | | <el-dialog title="用户数据导入" append-to-body v-model="excelBox" width="555px"> |
| | | <el-form :model="excelForm" label-width="100px"> |
| | | <el-form-item label="数据覆盖"> |
| | | <el-switch v-model="excelForm.isCovered" :active-value="1" :inactive-value="0" /> |
| | | </el-form-item> |
| | | <el-form-item label="模板上传"> |
| | | <el-upload |
| | | :action="uploadAction" |
| | | :on-success="handleUploadSuccess" |
| | | accept=".xls,.xlsx" |
| | | :show-file-list="false" |
| | | > |
| | | <el-button type="primary">点击上传</el-button> |
| | | </el-upload> |
| | | <div class="upload-tip">请上传 .xls,.xlsx 标准格式文件</div> |
| | | </el-form-item> |
| | | <el-form-item label="模板下载"> |
| | | <el-button type="primary" @click="handleTemplate">点击下载</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-dialog> |
| | | </basic-container> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, nextTick, onMounted, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useStore } from 'vuex' |
| | | import FormDiaLog from './FormDiaLog.vue' |
| | | import { getList as getListApi, remove, grant, resetPassword, unlock, auditPass, auditRefuse } from '@/api/system/user' |
| | | import { exportBlob } from '@/api/common' |
| | | import { getDeptTree, getDeptLazyTree } from '@/api/system/dept' |
| | | import { getRoleTree } from '@/api/system/role' |
| | | import website from '@/config/website' |
| | | import { getToken } from '@/utils/auth' |
| | | import { downloadXls } from '@/utils/util' |
| | | import NProgress from 'nprogress' |
| | | import 'nprogress/nprogress.css' |
| | | import func from '@/utils/func' |
| | | |
| | | const store = useStore() |
| | | const userInfo = computed(() => store.getters.userInfo) |
| | | const permission = computed(() => store.getters.permission) |
| | | |
| | | // 初始化搜索参数 |
| | | const initSearchParams = () => ({ |
| | | account: '', |
| | | realName: '', |
| | | current: 1, |
| | | size: 10, |
| | | }) |
| | | |
| | | const searchParams = ref(initSearchParams()) |
| | | const total = ref(0) |
| | | const loading = ref(false) |
| | | const list = ref([]) |
| | | const selectedIds = ref([]) |
| | | const selectionList = ref([]) |
| | | const queryParamsRef = ref(null) |
| | | const dialogRef = ref(null) |
| | | const dialogVisible = ref(false) |
| | | const auditMode = ref(false) |
| | | const treeDeptId = ref('') |
| | | |
| | | // 部门树 |
| | | const deptTree = ref([]) |
| | | const deptTreeProps = { |
| | | label: 'title', |
| | | children: 'children', |
| | | isLeaf: 'leaf', |
| | | } |
| | | |
| | | // 角色配置 |
| | | const roleBox = ref(false) |
| | | const roleGrantList = ref([]) |
| | | const roleTreeObj = ref([]) |
| | | const treeRoleRef = ref(null) |
| | | const roleProps = { |
| | | label: 'title', |
| | | value: 'key', |
| | | } |
| | | |
| | | // 导入相关 |
| | | const excelBox = ref(false) |
| | | const excelForm = ref({ |
| | | isCovered: 0, |
| | | }) |
| | | const uploadAction = computed(() => `/blade-system/user/import-user?isCovered=${excelForm.value.isCovered}`) |
| | | |
| | | provide('treeDeptId', treeDeptId) |
| | | |
| | | // 加载部门树节点 |
| | | async function loadDeptTree(node, resolve) { |
| | | const parentId = node.level === 0 ? 0 : node.data.id |
| | | const param = { |
| | | parentId: parentId, |
| | | level: node.level, |
| | | } |
| | | const res = await getDeptLazyTree(param) |
| | | resolve( |
| | | res.data.data.map(item => ({ |
| | | ...item, |
| | | leaf: !item.hasChildren, |
| | | })) |
| | | ) |
| | | } |
| | | |
| | | // 部门节点点击 |
| | | function handleDeptNodeClick(data) { |
| | | treeDeptId.value = data.id |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | // 获取列表 |
| | | async function getList() { |
| | | loading.value = true |
| | | try { |
| | | const res = await getListApi( |
| | | searchParams.value.current, |
| | | searchParams.value.size, |
| | | { |
| | | account: searchParams.value.account, |
| | | realName: searchParams.value.realName, |
| | | status: auditMode.value ? 0 : 1, |
| | | }, |
| | | treeDeptId.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 = initSearchParams() |
| | | treeDeptId.value = '' |
| | | getList() |
| | | } |
| | | |
| | | // 删除 |
| | | async function handleDelete(row) { |
| | | const tips = row ? '该条' : '选中的项' |
| | | await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', { type: 'warning' }) |
| | | const ids = row ? row.id : selectedIds.value.join(',') |
| | | await remove(ids) |
| | | ElMessage.success('删除成功') |
| | | selectedIds.value = [] |
| | | getList() |
| | | } |
| | | |
| | | // 勾选值设置 |
| | | function handleSelectionChange(rows) { |
| | | selectionList.value = rows |
| | | selectedIds.value = rows.map(item => item.id) |
| | | } |
| | | |
| | | // 角色配置 |
| | | async function handleGrant() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | roleTreeObj.value = [] |
| | | if (selectionList.value.length === 1) { |
| | | roleTreeObj.value = selectionList.value[0].roleId?.split(',') || [] |
| | | } |
| | | const res = await getRoleTree() |
| | | roleGrantList.value = res.data.data |
| | | roleBox.value = true |
| | | } |
| | | |
| | | // 提交角色配置 |
| | | async function submitRole() { |
| | | const roleList = treeRoleRef.value.getCheckedKeys().join(',') |
| | | await grant(selectedIds.value.join(','), roleList) |
| | | roleBox.value = false |
| | | ElMessage.success('操作成功') |
| | | getList() |
| | | } |
| | | |
| | | // 密码重置 |
| | | async function handleReset() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择账号密码重置为123456?', '提示', { type: 'warning' }) |
| | | await resetPassword(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | } |
| | | |
| | | // 账号解封 |
| | | async function handleLock() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择账号解封?', '提示', { type: 'warning' }) |
| | | await unlock(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | } |
| | | |
| | | // 审核通过 |
| | | async function handleAuditPass() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择数据通过审核?', '提示', { type: 'warning' }) |
| | | await auditPass(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | getList() |
| | | } |
| | | |
| | | // 审核拒绝 |
| | | async function handleAuditRefuse() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择数据拒绝审核?', '提示', { type: 'warning' }) |
| | | await auditRefuse(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | getList() |
| | | } |
| | | |
| | | // 返回正常模式 |
| | | function handleAuditBack() { |
| | | auditMode.value = false |
| | | getList() |
| | | } |
| | | |
| | | // 导入 |
| | | function handleImport() { |
| | | excelBox.value = true |
| | | } |
| | | |
| | | // 上传成功 |
| | | function handleUploadSuccess() { |
| | | excelBox.value = false |
| | | getList() |
| | | ElMessage.success('导入成功') |
| | | } |
| | | |
| | | // 导出 |
| | | function handleExport() { |
| | | const account = func.toStr(searchParams.value.account) |
| | | const realName = func.toStr(searchParams.value.realName) |
| | | ElMessageBox.confirm('是否导出用户数据?', '提示', { type: 'warning' }).then(() => { |
| | | NProgress.start() |
| | | exportBlob( |
| | | `/blade-system/user/export-user?${website.tokenHeader}=${getToken()}&account=${account}&realName=${realName}` |
| | | ).then(res => { |
| | | downloadXls(res.data, `用户数据表${new Date().toISOString().slice(0, 10)}.xlsx`) |
| | | NProgress.done() |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 下载模板 |
| | | function handleTemplate() { |
| | | exportBlob(`/blade-system/user/export-template?${website.tokenHeader}=${getToken()}`).then(res => { |
| | | downloadXls(res.data, '用户数据模板.xlsx') |
| | | }) |
| | | } |
| | | |
| | | // 新增/编辑/查看 弹框 |
| | | function openForm(mode, row) { |
| | | dialogVisible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode, row }) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .user-container { |
| | | height: 100%; |
| | | } |
| | | |
| | | .tree-box { |
| | | height: 100%; |
| | | padding: 10px; |
| | | border-radius: 5px; |
| | | background-color: #fff; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | |
| | | :deep(.el-scrollbar) { |
| | | height: 100%; |
| | | } |
| | | } |
| | | |
| | | .btn-group { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .upload-tip { |
| | | color: #999; |
| | | font-size: 12px; |
| | | margin-top: 8px; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <wel-container> |
| | | <div class="workbench" v-if="display"> |
| | | <div class="workleft"> |
| | | <!-- 设备统计 --> |
| | | <statistics></statistics> |
| | | <!-- 综合统计分析 --> |
| | | <div class="comprehensiveCon"> |
| | | <div class="comprehensive"> |
| | | <div> |
| | | |
| | | <div class="center"> |
| | | <div class="centerLeft"> |
| | | <!-- 工单统计 --> |
| | | <proportionStatic></proportionStatic> |
| | | <!-- 飞行统计 --> |
| | | <flightStatistics></flightStatistics> |
| | | |
| | | </div> |
| | | <div class="centerRight"> |
| | | <!-- 机巢工单数量排名(件) --> |
| | | <flyratio></flyratio> |
| | | <!-- 任务成果 --> |
| | | <taskOutcome></taskOutcome> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="workright"> |
| | | <Bocklog></Bocklog> |
| | | <CalenBox></CalenBox> |
| | | </div> |
| | | </div> |
| | | </wel-container> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import taskOutcome from './components/taskOutcome.vue' |
| | | import flightStatistics from './components/flightStatistics.vue' |
| | | import proportionStatic from './components/proportionStatic.vue' |
| | | import flyratio from './components/flyratio.vue' |
| | | import Bocklog from './components/backlog.vue' |
| | | import CalenBox from './components/calendarBox.vue' |
| | | import * as echarts from 'echarts' |
| | | import useEchartsResize from '@/hooks/useEchartsResize' |
| | | import { mapGetters } from 'vuex' |
| | | import { getJobEventByStatus, getJobEventTotal, getFly, getFlyTime } from '@/api/home/index' |
| | | import overviewImg1 from '@/assets/images/workbench/tc1.png' |
| | | |
| | | import fy1 from '@/assets/images/workbench/fy1.png' |
| | | |
| | | import statistics from './components/statistics.vue' |
| | | import { ElMessage } from 'element-plus' |
| | | let checked = ref('CURRENT_YEAR') |
| | | const display = ref(false) |
| | | let timeListStr = ['本周', '本月', '本年'] |
| | | let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR'] |
| | | const params = ref({ |
| | | date_enum: 'CURRENT_YEAR', |
| | | device_sn: '', |
| | | end_date: undefined, |
| | | start_date: undefined, |
| | | }) |
| | | const dateSelect = ref('CURRENT_YEAR') |
| | | let timeClick = (item, index) => { |
| | | checked.value = item |
| | | params.value.date_enum = item |
| | | dateSelect.value = item |
| | | |
| | | |
| | | |
| | | } |
| | | const refresh = () => { |
| | | params.value.date_enum = 'CURRENT_YEAR' |
| | | checked.value = 'CURRENT_YEAR' |
| | | dateSelect.value = 'CURRENT_YEAR' |
| | | |
| | | |
| | | } |
| | | // 跳转 |
| | | const jumpshebei = () => { |
| | | ElMessage.warning('加急开发中...') |
| | | } |
| | | |
| | | const eventTotal = ref(0) |
| | | const data = ref([]) |
| | | |
| | | onMounted(() => { |
| | | getJobEventTotal().then(res => { |
| | | eventTotal.value = res?.data?.data || 0 |
| | | }) |
| | | setTimeout(() => { |
| | | display.value = true |
| | | },100) |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | .el-font-size { |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | <style scoped lang="scss"> |
| | | .workbench { |
| | | height: 100%; |
| | | flex: 1; |
| | | // padding: 0px 20px 0 10px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | <style lang="scss" scoped> |
| | | |
| | | .workleft { |
| | | width: 68%; |
| | | margin-right: 10px; |
| | | height: 100%; |
| | | |
| | | .comprehensiveCon { |
| | | // background: #ffffff !important; |
| | | height: pxToVh(776); |
| | | border-radius: 8px 8px 8px 8px; |
| | | |
| | | .comprehensive { |
| | | |
| | | // padding: 14px 14px 0 21px; |
| | | |
| | | .title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .name { |
| | | display: flex; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | color: #363636; |
| | | font-family: 'Source Han Sans CN'; |
| | | |
| | | span { |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | img { |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | .arrow { |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .time-card { |
| | | text-align: center; |
| | | // height: 30px; |
| | | height: pxToVh(30); |
| | | background: #ffffff; |
| | | border-radius: 4px 0px 0px 4px; |
| | | border: 1px solid #e5e5e5; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #7c8091; |
| | | display: flex; |
| | | width: 282px; |
| | | margin-left: 15px; |
| | | |
| | | .card-item { |
| | | width: 94px; |
| | | height: 100%; |
| | | line-height: 28px; |
| | | cursor: pointer; |
| | | font-family: 'Source Han Sans CN'; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #7c8091; |
| | | } |
| | | |
| | | .card-item:first-child { |
| | | border-right: 1px solid #e5e5e5; |
| | | } |
| | | |
| | | .card-item:nth-child(2) { |
| | | border-right: 1px solid #e5e5e5; |
| | | } |
| | | |
| | | .card-item.active { |
| | | color: #1441ff; |
| | | border: 1px solid #1c5cff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 工、单 |
| | | .center { |
| | | display: flex; |
| | | .centerLeft { |
| | | width: 50%; |
| | | |
| | | |
| | | } |
| | | |
| | | .centerRight { |
| | | width: 50%; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .workright { |
| | | width: 0; |
| | | flex: 1; |
| | | height: 100%; |
| | | // display: flex; |
| | | // flex-direction: column; |
| | | } |
| | | </style> |
| | | </style> |