55 changed files with 8541 additions and 33 deletions
@ -0,0 +1,56 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS警报规则信息 */ |
|||
export interface AlarmRule { |
|||
id: number // 主键ID
|
|||
gasTypeId?: number // 气体类型ID
|
|||
alarmTypeId?: number // 警报类型ID
|
|||
alarmName: string // 警报名称
|
|||
alarmNameColor: string // 警报名称颜色
|
|||
alarmColor: string // 警报颜色
|
|||
alarmLevel: number // 警报方式/级别(0:正常状态;1:一级警报;2:二级警报;3:弹窗警报)
|
|||
min: number // 触发值(小)
|
|||
max: number // 触发值(大)
|
|||
direction?: number // 最值方向(0:小;1:大)
|
|||
sortOrder?: number // 排序
|
|||
remark: string // 备注
|
|||
} |
|||
|
|||
// GAS警报规则 API
|
|||
export const AlarmRuleApi = { |
|||
// 查询GAS警报规则分页
|
|||
getAlarmRulePage: async (params: any) => { |
|||
return await request.get({ url: `/gas/alarm-rule/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS警报规则详情
|
|||
getAlarmRule: async (id: number) => { |
|||
return await request.get({ url: `/gas/alarm-rule/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS警报规则
|
|||
createAlarmRule: async (data: AlarmRule) => { |
|||
return await request.post({ url: `/gas/alarm-rule/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS警报规则
|
|||
updateAlarmRule: async (data: AlarmRule) => { |
|||
return await request.put({ url: `/gas/alarm-rule/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS警报规则
|
|||
deleteAlarmRule: async (id: number) => { |
|||
return await request.delete({ url: `/gas/alarm-rule/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS警报规则 */ |
|||
deleteAlarmRuleList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/alarm-rule/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS警报规则 Excel
|
|||
exportAlarmRule: async (params) => { |
|||
return await request.download({ url: `/gas/alarm-rule/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS警报类型信息 */ |
|||
export interface AlarmType { |
|||
id: number // 主键ID
|
|||
name?: string // 名称
|
|||
nameColor: string // 名称颜色
|
|||
color?: string // 颜色
|
|||
level?: number // 警报方式/级别(0:正常状态;1:一级警报;2:二级警报;3:弹窗警报)
|
|||
sortOrder?: number // 排序
|
|||
remark: string // 备注
|
|||
} |
|||
|
|||
// GAS警报类型 API
|
|||
export const AlarmTypeApi = { |
|||
// 查询GAS警报类型分页
|
|||
getAlarmTypePage: async (params: any) => { |
|||
return await request.get({ url: `/gas/alarm-type/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS警报类型详情
|
|||
getAlarmType: async (id: number) => { |
|||
return await request.get({ url: `/gas/alarm-type/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS警报类型
|
|||
createAlarmType: async (data: AlarmType) => { |
|||
return await request.post({ url: `/gas/alarm-type/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS警报类型
|
|||
updateAlarmType: async (data: AlarmType) => { |
|||
return await request.put({ url: `/gas/alarm-type/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS警报类型
|
|||
deleteAlarmType: async (id: number) => { |
|||
return await request.delete({ url: `/gas/alarm-type/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS警报类型 */ |
|||
deleteAlarmTypeList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/alarm-type/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS警报类型 Excel
|
|||
exportAlarmType: async (params) => { |
|||
return await request.download({ url: `/gas/alarm-type/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,64 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS工厂信息 */ |
|||
export interface Factory { |
|||
id: number // 主键ID
|
|||
parentId?: number // 父节点ID
|
|||
type: number // 层级(1:工厂;2:车间;3:班组)
|
|||
name?: string // 名称
|
|||
city: string // 城市
|
|||
alarmTotal?: number // 总警报数
|
|||
alarmDeal?: number // 已处理警报数
|
|||
picUrl: string // 区域图
|
|||
picScale: number // 区域图缩放比例
|
|||
picX: number // 在区域图X坐标值
|
|||
picY: number // 在区域图X坐标值
|
|||
longitude: number // 经度
|
|||
latitude: number // 纬度
|
|||
rectSouthWest: string // 区域西南坐标
|
|||
rectNorthEast: string // 区域东北坐标
|
|||
sortOrder?: number // 排序
|
|||
remark: string // 备注
|
|||
delFlag?: number // 删除标志
|
|||
createBy?: string // 创建者
|
|||
updateBy: string // 更新者
|
|||
} |
|||
|
|||
// GAS工厂 API
|
|||
export const FactoryApi = { |
|||
// 查询GAS工厂分页
|
|||
getFactoryPage: async (params: any) => { |
|||
return await request.get({ url: `/gas/factory/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS工厂详情
|
|||
getFactory: async (id: number) => { |
|||
return await request.get({ url: `/gas/factory/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS工厂
|
|||
createFactory: async (data: Factory) => { |
|||
return await request.post({ url: `/gas/factory/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS工厂
|
|||
updateFactory: async (data: Factory) => { |
|||
return await request.put({ url: `/gas/factory/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS工厂
|
|||
deleteFactory: async (id: number) => { |
|||
return await request.delete({ url: `/gas/factory/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS工厂 */ |
|||
deleteFactoryList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/factory/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS工厂 Excel
|
|||
exportFactory: async (params) => { |
|||
return await request.download({ url: `/gas/factory/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,50 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS电子围栏信息 */ |
|||
export interface Fence { |
|||
id: number // 主键ID
|
|||
name: string // 围栏名称
|
|||
fenceRange: string // 围栏范围
|
|||
status: number // 状态(1启用,2禁用)
|
|||
type: number // 围栏类型(1:超出报警,2:进入报警)
|
|||
remark: string // 备注
|
|||
} |
|||
|
|||
// GAS电子围栏 API
|
|||
export const FenceApi = { |
|||
// 查询GAS电子围栏分页
|
|||
getFencePage: async (params: any) => { |
|||
return await request.get({ url: `/gas/fence/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS电子围栏详情
|
|||
getFence: async (id: number) => { |
|||
return await request.get({ url: `/gas/fence/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS电子围栏
|
|||
createFence: async (data: Fence) => { |
|||
return await request.post({ url: `/gas/fence/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS电子围栏
|
|||
updateFence: async (data: Fence) => { |
|||
return await request.put({ url: `/gas/fence/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS电子围栏
|
|||
deleteFence: async (id: number) => { |
|||
return await request.delete({ url: `/gas/fence/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS电子围栏 */ |
|||
deleteFenceList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/fence/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS电子围栏 Excel
|
|||
exportFence: async (params) => { |
|||
return await request.download({ url: `/gas/fence/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS手持探测器围栏报警信息 */ |
|||
export interface FenceAlarm { |
|||
id: number // 主键ID
|
|||
detectorId?: number // 探头ID
|
|||
fenceId: number // 围栏id
|
|||
type: number // 报警类型
|
|||
picX: number // 在区域图X坐标值
|
|||
picY: number // 在区域图X坐标值
|
|||
distance: number // 超出围栏米数
|
|||
maxDistance: number // 最远超出米数
|
|||
tAlarmStart: string | Dayjs // 开始时间
|
|||
tAlarmEnd: string | Dayjs // 结束时间
|
|||
status: number // 状态(0:待处理;1:处理中;1:已处理)
|
|||
remark: string // 备注
|
|||
} |
|||
|
|||
// GAS手持探测器围栏报警 API
|
|||
export const FenceAlarmApi = { |
|||
// 查询GAS手持探测器围栏报警分页
|
|||
getFenceAlarmPage: async (params: any) => { |
|||
return await request.get({ url: `/gas/fence-alarm/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS手持探测器围栏报警详情
|
|||
getFenceAlarm: async (id: number) => { |
|||
return await request.get({ url: `/gas/fence-alarm/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS手持探测器围栏报警
|
|||
createFenceAlarm: async (data: FenceAlarm) => { |
|||
return await request.post({ url: `/gas/fence-alarm/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS手持探测器围栏报警
|
|||
updateFenceAlarm: async (data: FenceAlarm) => { |
|||
return await request.put({ url: `/gas/fence-alarm/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS手持探测器围栏报警
|
|||
deleteFenceAlarm: async (id: number) => { |
|||
return await request.delete({ url: `/gas/fence-alarm/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS手持探测器围栏报警 */ |
|||
deleteFenceAlarmList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/fence-alarm/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS手持探测器围栏报警 Excel
|
|||
exportFenceAlarm: async (params) => { |
|||
return await request.download({ url: `/gas/fence-alarm/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,50 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS气体信息 */ |
|||
export interface Type { |
|||
id: number // 主键ID
|
|||
name?: string // 名称
|
|||
chemical?: string // 化学式
|
|||
unit: string // 单位
|
|||
sortOrder?: number // 排序
|
|||
remark: string // 备注
|
|||
} |
|||
|
|||
// GAS气体 API
|
|||
export const TypeApi = { |
|||
// 查询GAS气体分页
|
|||
getTypePage: async (params: any) => { |
|||
return await request.get({ url: `/gas/type/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS气体详情
|
|||
getType: async (id: number) => { |
|||
return await request.get({ url: `/gas/type/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS气体
|
|||
createType: async (data: Type) => { |
|||
return await request.post({ url: `/gas/type/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS气体
|
|||
updateType: async (data: Type) => { |
|||
return await request.put({ url: `/gas/type/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS气体
|
|||
deleteType: async (id: number) => { |
|||
return await request.delete({ url: `/gas/type/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS气体 */ |
|||
deleteTypeList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/type/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS气体 Excel
|
|||
exportType: async (params) => { |
|||
return await request.download({ url: `/gas/type/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,60 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS手持探测器警报信息 */ |
|||
export interface HandAlarm { |
|||
id: number // 主键ID
|
|||
detectorId: number // 手持表id
|
|||
sn: string // 设备编号
|
|||
alarmType: number // 报警类型
|
|||
alarmLevel?: number // 警报方式/级别(0:正常状态;1:一级警报;2:二级警报;3:弹窗警报)
|
|||
gasType?: string // 气体类型
|
|||
unit?: string // 单位
|
|||
location: string // 位置描述
|
|||
picX: number // 在区域图X坐标值
|
|||
picY?: number // 在区域图X坐标值
|
|||
vAlarmFirst: number // 首报值
|
|||
vAlarmMaximum: number // 最值
|
|||
tAlarmStart: string | Dayjs // 开始时间
|
|||
tAlarmEnd: string | Dayjs // 结束时间
|
|||
status: number // 状态(0:待处理;1:处理中;1:已处理)
|
|||
remark?: string // 备注
|
|||
} |
|||
|
|||
// GAS手持探测器警报 API
|
|||
export const HandAlarmApi = { |
|||
// 查询GAS手持探测器警报分页
|
|||
getHandAlarmPage: async (params: any) => { |
|||
return await request.get({ url: `/gas/hand-alarm/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS手持探测器警报详情
|
|||
getHandAlarm: async (id: number) => { |
|||
return await request.get({ url: `/gas/hand-alarm/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS手持探测器警报
|
|||
createHandAlarm: async (data: HandAlarm) => { |
|||
return await request.post({ url: `/gas/hand-alarm/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS手持探测器警报
|
|||
updateHandAlarm: async (data: HandAlarm) => { |
|||
return await request.put({ url: `/gas/hand-alarm/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS手持探测器警报
|
|||
deleteHandAlarm: async (id: number) => { |
|||
return await request.delete({ url: `/gas/hand-alarm/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS手持探测器警报 */ |
|||
deleteHandAlarmList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/hand-alarm/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS手持探测器警报 Excel
|
|||
exportHandAlarm: async (params) => { |
|||
return await request.download({ url: `/gas/hand-alarm/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,63 @@ |
|||
import request from '@/config/axios' |
|||
import type { Dayjs } from 'dayjs' |
|||
|
|||
/** GAS手持探测器信息 */ |
|||
export interface HandDetector { |
|||
id: number // 主键ID
|
|||
sn?: string // SN
|
|||
name?: string // 持有人
|
|||
fenceIds?: string // 围栏ids
|
|||
fenceIdsArray?: string[] // 围栏ids数组
|
|||
gasTypeId?: number // 气体类型ID
|
|||
gasChemical?: string // 气体化学式
|
|||
min?: number // 测量范围中的最小值
|
|||
max?: number // 测量范围中的最大值
|
|||
unit?: string // 单位
|
|||
model?: string // 设备型号
|
|||
manufacturer?: string // 生产厂家
|
|||
batteryAlarmValue?: number // 低于多少电量报警
|
|||
enableStatus?: number // 启用状态(0:备用;1:启用)
|
|||
longitude?: number // 经度
|
|||
latitude?: number // 纬度
|
|||
accuracy?: number // 数值除数
|
|||
sortOrder?: number // 排序
|
|||
remark?: string // 备注
|
|||
} |
|||
|
|||
// GAS手持探测器 API
|
|||
export const HandDetectorApi = { |
|||
// 查询GAS手持探测器分页
|
|||
getHandDetectorPage: async (params: any) => { |
|||
return await request.get({ url: `/gas/hand-detector/page`, params }) |
|||
}, |
|||
|
|||
// 查询GAS手持探测器详情
|
|||
getHandDetector: async (id: number) => { |
|||
return await request.get({ url: `/gas/hand-detector/get?id=` + id }) |
|||
}, |
|||
|
|||
// 新增GAS手持探测器
|
|||
createHandDetector: async (data: HandDetector) => { |
|||
return await request.post({ url: `/gas/hand-detector/create`, data }) |
|||
}, |
|||
|
|||
// 修改GAS手持探测器
|
|||
updateHandDetector: async (data: HandDetector) => { |
|||
return await request.put({ url: `/gas/hand-detector/update`, data }) |
|||
}, |
|||
|
|||
// 删除GAS手持探测器
|
|||
deleteHandDetector: async (id: number) => { |
|||
return await request.delete({ url: `/gas/hand-detector/delete?id=` + id }) |
|||
}, |
|||
|
|||
/** 批量删除GAS手持探测器 */ |
|||
deleteHandDetectorList: async (ids: number[]) => { |
|||
return await request.delete({ url: `/gas/hand-detector/delete-list?ids=${ids.join(',')}` }) |
|||
}, |
|||
|
|||
// 导出GAS手持探测器 Excel
|
|||
exportHandDetector: async (params) => { |
|||
return await request.download({ url: `/gas/hand-detector/export-excel`, params }) |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
import request from '@/config/axios' |
|||
|
|||
const getLastestDetectorData = async () => { |
|||
const data = await request.get({ url: `/gas/hand-detector/getByHandData` }) |
|||
return Object.values(data) |
|||
} |
|||
export { getLastestDetectorData } |
@ -0,0 +1,81 @@ |
|||
import { defineStore } from 'pinia' |
|||
import { store } from '@/store' |
|||
import { HandDetector, HandDetectorApi } from '@/api/gas/handdetector' |
|||
import { Type, TypeApi } from '@/api/gas/gastype' |
|||
import { Fence, FenceApi } from '@/api/gas/fence' |
|||
import { AlarmType, AlarmTypeApi } from '@/api/gas/alarmtype' |
|||
|
|||
export const useHandDetectorStore = defineStore('handDetector', { |
|||
state() { |
|||
return { |
|||
handDetectorList: [] as HandDetector[], |
|||
gasTypes: [] as Type[], |
|||
fences: [] as Fence[], |
|||
alarmTypes: [] as AlarmType[] |
|||
} |
|||
}, |
|||
getters: { |
|||
getHandDetectorList(): HandDetector[] { |
|||
return this.handDetectorList |
|||
}, |
|||
getGasTypes(): Type[] { |
|||
return this.gasTypes |
|||
}, |
|||
getFences(): Fence[] { |
|||
return this.fences |
|||
}, |
|||
getAlarmTypes(): AlarmType[] { |
|||
return this.alarmTypes |
|||
} |
|||
}, |
|||
actions: { |
|||
async getAllHandDetector(refresh: boolean = false) { |
|||
if (refresh || this.handDetectorList.length === 0) { |
|||
const data = await HandDetectorApi.getHandDetectorPage({ |
|||
pageNo: 1, |
|||
pageSize: 100 |
|||
}) |
|||
this.handDetectorList = data.list |
|||
return this.handDetectorList |
|||
} else { |
|||
return this.handDetectorList |
|||
} |
|||
}, |
|||
async getAllFences(refresh: boolean = false) { |
|||
if (refresh || this.fences.length === 0) { |
|||
const data = await FenceApi.getFencePage({ |
|||
pageNo: 1, |
|||
pageSize: 100 |
|||
}) |
|||
this.fences = data.list |
|||
return this.fences |
|||
} else { |
|||
return this.fences |
|||
} |
|||
}, |
|||
async getAllGasTypes(refresh: boolean = false) { |
|||
if (refresh || this.gasTypes.length === 0) { |
|||
const data = await TypeApi.getTypePage({ |
|||
pageNo: 1, |
|||
pageSize: 100 |
|||
}) |
|||
this.gasTypes = data.list |
|||
return this.gasTypes |
|||
} else { |
|||
return this.gasTypes |
|||
} |
|||
}, |
|||
async getAllAlarmTypes(refresh: boolean = false) { |
|||
if (refresh || this.alarmTypes.length === 0) { |
|||
const data = await AlarmTypeApi.getAlarmTypePage({ |
|||
pageNo: 1, |
|||
pageSize: 100 |
|||
}) |
|||
this.alarmTypes = data.list |
|||
return this.alarmTypes |
|||
} else { |
|||
return this.alarmTypes |
|||
} |
|||
} |
|||
} |
|||
}) |
@ -0,0 +1,4 @@ |
|||
<template>历史数据</template> |
|||
<script setup lang="ts"> |
|||
defineOptions({ name: 'HandDeviceHistory' }) |
|||
</script> |
@ -0,0 +1,139 @@ |
|||
<template> |
|||
<div class="map-controls"> |
|||
<el-button |
|||
v-if="showMarkers" |
|||
:type="isMarkersActive ? 'primary' : 'default'" |
|||
@click="$emit('toggle-markers')" |
|||
class="control-btn" |
|||
> |
|||
<el-icon><MapLocation /></el-icon> |
|||
</el-button> |
|||
|
|||
<el-button |
|||
v-if="showFences" |
|||
:type="isFencesActive ? 'primary' : 'default'" |
|||
@click="$emit('toggle-fences')" |
|||
class="control-btn" |
|||
> |
|||
<el-icon><Menu /></el-icon> |
|||
</el-button> |
|||
|
|||
<el-button |
|||
v-if="showTrajectories" |
|||
:type="isTrajectoriesActive ? 'primary' : 'default'" |
|||
@click="$emit('toggle-trajectories')" |
|||
class="control-btn" |
|||
> |
|||
<el-icon><Timer /></el-icon> |
|||
</el-button> |
|||
|
|||
<el-button |
|||
v-if="showDrawFences" |
|||
:type="isDrawFencesActive ? 'primary' : 'default'" |
|||
@click="$emit('toggle-draw-fences')" |
|||
class="control-btn" |
|||
> |
|||
<el-icon><Edit /></el-icon> |
|||
</el-button> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { Timer, MapLocation, Menu, Edit } from '@element-plus/icons-vue' |
|||
|
|||
interface Props { |
|||
/** 是否显示标记控制按钮 */ |
|||
showMarkers?: boolean |
|||
/** 是否显示围栏控制按钮 */ |
|||
showFences?: boolean |
|||
/** 是否显示轨迹控制按钮 */ |
|||
showTrajectories?: boolean |
|||
/** 是否显示绘制围栏控制按钮 */ |
|||
showDrawFences?: boolean |
|||
/** 标记按钮是否激活 */ |
|||
isMarkersActive?: boolean |
|||
/** 围栏按钮是否激活 */ |
|||
isFencesActive?: boolean |
|||
/** 轨迹按钮是否激活 */ |
|||
isTrajectoriesActive?: boolean |
|||
/** 绘制围栏按钮是否激活 */ |
|||
isDrawFencesActive?: boolean |
|||
} |
|||
|
|||
interface Emits { |
|||
(e: 'toggle-markers'): void |
|||
(e: 'toggle-fences'): void |
|||
(e: 'toggle-trajectories'): void |
|||
(e: 'toggle-draw-fences'): void |
|||
} |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
showMarkers: true, |
|||
showFences: true, |
|||
showTrajectories: true, |
|||
showDrawFences: true, |
|||
isMarkersActive: false, |
|||
isFencesActive: false, |
|||
isTrajectoriesActive: false, |
|||
isDrawFencesActive: false |
|||
}) |
|||
|
|||
defineEmits<Emits>() |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.map-controls { |
|||
position: absolute; |
|||
padding-left: 20px; |
|||
top: 150px; |
|||
left: 20px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
z-index: 1000; |
|||
} |
|||
|
|||
.control-btn { |
|||
width: 40px; |
|||
height: 40px; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|||
background: rgba(255, 255, 255, 0.95); |
|||
backdrop-filter: blur(10px); |
|||
border: 1px solid rgba(255, 255, 255, 0.2); |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.control-btn:hover { |
|||
transform: translateY(-2px); |
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|||
} |
|||
|
|||
.control-btn.el-button--primary { |
|||
background: #409eff; |
|||
} |
|||
|
|||
.control-btn.el-button--primary:hover { |
|||
background: #337ecc; |
|||
} |
|||
.el-button + .el-button { |
|||
margin-left: 0; |
|||
} |
|||
@media (max-width: 768px) { |
|||
.map-controls { |
|||
flex-direction: row; |
|||
padding-left: 0; |
|||
top: 10px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
} |
|||
|
|||
.control-btn { |
|||
width: 36px; |
|||
height: 36px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,510 @@ |
|||
<template> |
|||
<div class="map-container" ref="mapContainerRef"> |
|||
<MapControls |
|||
:show-markers="props.showMarkers" |
|||
:show-fences="props.showFences" |
|||
:show-trajectories="props.showTrajectories" |
|||
:show-draw-fences="props.showDrawFences" |
|||
:is-markers-active="showMarkers" |
|||
:is-fences-active="showFences" |
|||
:is-trajectories-active="showTrajectories" |
|||
:is-draw-fences-active="showDrawFences" |
|||
@toggle-markers="toggleMarkers" |
|||
@toggle-fences="toggleFences" |
|||
@toggle-trajectories="toggleTrajectories" |
|||
@toggle-draw-fences="toggleDrawFences" |
|||
/> |
|||
<TrajectoryControls |
|||
:show-controls="showTrajectories" |
|||
:play-state="trajectoryPlayState" |
|||
@play="playTrajectory" |
|||
@pause="pauseTrajectory" |
|||
@stop="stopTrajectory" |
|||
@speed-change="setTrajectorySpeed" |
|||
@time-change="setTrajectoryTime" |
|||
@time-range-change="setTrajectoryTimeRange" |
|||
/> |
|||
<div class="top-panel" v-show="!appStore.mobile"> |
|||
<div class="top-panel__left"> |
|||
<div class="search-group"> |
|||
<el-input v-model="search" class="search-input" placeholder="请输入关键词" /> |
|||
</div> |
|||
</div> |
|||
<div class="top-panel__center"> |
|||
<div class="data_item"> |
|||
<div class="data_item__title">手持设备</div> |
|||
<div class="data_item__value">2000<span class="data_item__unit">台</span></div> |
|||
</div> |
|||
<div class="data_item"> |
|||
<div class="data_item__title">在线数量</div> |
|||
<div class="data_item__value">200<span class="data_item__unit">台</span></div> |
|||
</div> |
|||
<div class="data_item"> |
|||
<div class="data_item__title">用户数量</div> |
|||
<div class="data_item__value">200<span class="data_item__unit">人</span></div> |
|||
</div> |
|||
<div class="data_item"> |
|||
<div class="data_item__title">企业数量</div> |
|||
<div class="data_item__value">20<span class="data_item__unit">家</span></div> |
|||
</div> |
|||
</div> |
|||
<div class="top-panel__right"> |
|||
<span class="legend-title">报警图例:</span> |
|||
<div class="normal-legend">正常状态</div> |
|||
<div class="alarm1-legend">围栏报警</div> |
|||
<div class="alarm2-legend">气体报警</div> |
|||
</div> |
|||
</div> |
|||
<div v-if="panelVisible" class="info-panel"> |
|||
<div class="info-panel__header"> |
|||
<span class="info-panel__title">设备详情</span> |
|||
<button class="info-panel__close" @click="panelVisible = false">×</button> |
|||
</div> |
|||
<div class="info-panel__body"> |
|||
<div v-if="selectedMarker"> |
|||
<div class="info-panel__name">{{ selectedMarker.name }}</div> |
|||
<div class="info-panel__coord" |
|||
>坐标:{{ selectedMarker.coordinates[0].toFixed(6) }}, |
|||
{{ selectedMarker.coordinates[1].toFixed(6) }}</div |
|||
> |
|||
</div> |
|||
<div v-else class="info-panel__empty">未选择设备</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { useAppStore } from '@/store/modules/app' |
|||
import { useHandDetectorStore } from '@/store/modules/handDetector' |
|||
import { ref, onMounted, watch } from 'vue' |
|||
import { ElMessage } from 'element-plus' |
|||
// 导入类型定义 |
|||
import type { MapProps, MarkerData } from './types/map.types' |
|||
|
|||
// 导入常量配置 |
|||
import { MAP_DEFAULTS, DEFAULT_MARKERS, DEFAULT_FENCES } from './constants/map.constants' |
|||
|
|||
// 导入组合式函数 |
|||
import { useMapServices } from './composables/useMapServices' |
|||
import { useMapEvents } from './composables/useMapEvents' |
|||
import { useTrajectoryControls } from './composables/useTrajectoryControls' |
|||
import { useMapWatchers } from './composables/useMapWatchers' |
|||
|
|||
// 导入组件 |
|||
import TrajectoryControls from './TrajectoryControls.vue' |
|||
import MapControls from './MapControls.vue' |
|||
const props = withDefaults(defineProps<MapProps>(), { |
|||
tileUrl: MAP_DEFAULTS.tileUrl, |
|||
center: () => MAP_DEFAULTS.center, |
|||
zoom: MAP_DEFAULTS.zoom, |
|||
maxZoom: MAP_DEFAULTS.maxZoom, |
|||
minZoom: MAP_DEFAULTS.minZoom, |
|||
markers: () => DEFAULT_MARKERS, |
|||
fences: () => DEFAULT_FENCES, |
|||
enableCluster: MAP_DEFAULTS.enableCluster, |
|||
clusterDistance: MAP_DEFAULTS.clusterDistance, |
|||
showTrajectories: true, |
|||
showMarkers: true, |
|||
showFences: true, |
|||
showDrawFences: true |
|||
}) |
|||
// 响应式状态 |
|||
const showMarkers = ref(props.showMarkers) |
|||
const showTrajectories = ref(false) |
|||
const showFences = ref(false) |
|||
const showDrawFences = ref(false) |
|||
const mapContainerRef = ref<HTMLElement | null>(null) |
|||
const handDetectorStore = useHandDetectorStore() |
|||
// 左侧信息面板状态 |
|||
const appStore = useAppStore() |
|||
const panelVisible = ref(false) |
|||
const selectedMarker = ref<MarkerData | null>(null) |
|||
const search = ref('') |
|||
|
|||
// 使用组合式函数 |
|||
const { |
|||
services, |
|||
layerRefs, |
|||
initializeServices, |
|||
initializeMapAndLayers, |
|||
setMarkersVisible, |
|||
setTrajectoriesVisible, |
|||
setFencesVisible, |
|||
toggleFenceDrawing, |
|||
clearFenceDrawLayer, |
|||
updateMarkers, |
|||
refreshMarkerStyles |
|||
} = useMapServices() |
|||
|
|||
const { |
|||
trajectoryPlayState, |
|||
playTrajectory, |
|||
pauseTrajectory, |
|||
stopTrajectory, |
|||
setTrajectorySpeed, |
|||
setTrajectoryTime, |
|||
setTrajectoryTimeRange, |
|||
setupTrajectoryWatcher, |
|||
cleanup: cleanupTrajectory |
|||
} = useTrajectoryControls() |
|||
|
|||
const { setupMapEventListeners } = useMapEvents() |
|||
|
|||
// 控制函数 |
|||
const toggleTrajectories = () => { |
|||
if (showTrajectories.value && trajectoryPlayState.value.isPlaying) { |
|||
cleanupTrajectory() |
|||
} |
|||
showTrajectories.value = !showTrajectories.value |
|||
} |
|||
|
|||
const toggleMarkers = () => { |
|||
showMarkers.value = !showMarkers.value |
|||
} |
|||
|
|||
const toggleFences = () => { |
|||
showFences.value = !showFences.value |
|||
} |
|||
|
|||
const toggleDrawFences = () => { |
|||
showDrawFences.value = !showDrawFences.value |
|||
|
|||
if (showDrawFences.value) { |
|||
// 开始绘制围栏 |
|||
toggleFenceDrawing(true, handleFenceDrawComplete) |
|||
} else { |
|||
// 停止绘制围栏 |
|||
toggleFenceDrawing(false) |
|||
} |
|||
} |
|||
|
|||
import { MapService } from './services/map.service' |
|||
let mapService: MapService | null = null |
|||
let isMapInitialized = false |
|||
/** |
|||
* 初始化地图 |
|||
*/ |
|||
const initMap = () => { |
|||
if (!mapContainerRef.value) return |
|||
|
|||
// 初始化服务 |
|||
mapService = new MapService() |
|||
|
|||
// 初始化地图 |
|||
|
|||
try { |
|||
// 初始化服务 |
|||
initializeServices() |
|||
const mapInstance = mapService.initMap(mapContainerRef.value, props) |
|||
// 初始化地图和图层 |
|||
const { map, popupOverlay } = initializeMapAndLayers(mapInstance, props) |
|||
|
|||
// 设置初始显示状态 |
|||
setMarkersVisible(showMarkers.value) |
|||
setTrajectoriesVisible(showTrajectories.value, props.markers) |
|||
setFencesVisible(showFences.value) |
|||
|
|||
// 设置事件监听器 |
|||
setupMapEventListeners( |
|||
map, |
|||
popupOverlay, |
|||
services.trajectoryService as any, |
|||
services.popupService, |
|||
{ |
|||
isDrawing: () => !!services.fenceDrawService?.isCurrentlyDrawing?.(), |
|||
onMarkerClick: (marker: MarkerData) => { |
|||
selectedMarker.value = marker |
|||
panelVisible.value = true |
|||
}, |
|||
markerLayer: layerRefs.value?.markerLayer, |
|||
refreshMarkerStyles |
|||
} |
|||
) |
|||
// 设置轨迹监听器 |
|||
setupTrajectoryWatcher(services.trajectoryService as any, showTrajectories) |
|||
|
|||
// 设置状态监听器 |
|||
const { setupAllWatchers } = useMapWatchers({ |
|||
showMarkers, |
|||
showTrajectories, |
|||
showFences, |
|||
showDrawFences, |
|||
setMarkersVisible, |
|||
setTrajectoriesVisible, |
|||
setFencesVisible, |
|||
toggleFenceDrawing, |
|||
updateMarkers, |
|||
markers: props.markers || [] |
|||
}) |
|||
|
|||
setupAllWatchers() |
|||
|
|||
// 标记地图已初始化 |
|||
isMapInitialized = true |
|||
|
|||
console.log('地图初始化成功', { map, services: services }) |
|||
} catch (error) { |
|||
console.error('地图初始化失败:', error) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理围栏绘制完成 |
|||
*/ |
|||
const handleFenceDrawComplete = (coordinates: [number, number][]) => { |
|||
if (coordinates.length < 3) { |
|||
// 围栏至少需要3个点 |
|||
ElMessage.warning('围栏至少需要3个点才能形成有效区域') |
|||
return |
|||
} |
|||
console.log('围栏绘制完成:', coordinates) |
|||
clearFenceDrawLayer() |
|||
// 重置绘制状态 |
|||
showDrawFences.value = false |
|||
} |
|||
// 监听 markers props 变化 |
|||
watch( |
|||
() => props.markers, |
|||
(newMarkers) => { |
|||
if (newMarkers && newMarkers.length > 0 && isMapInitialized) { |
|||
updateMarkers(newMarkers, props) |
|||
} |
|||
}, |
|||
{ deep: true, immediate: false } |
|||
) |
|||
|
|||
onMounted(() => { |
|||
setTimeout(() => { |
|||
initMap() |
|||
}, 100) |
|||
}) |
|||
</script> |
|||
<style scoped> |
|||
.map-container { |
|||
width: 100%; |
|||
height: calc(100vh - 120px); |
|||
} |
|||
|
|||
:deep(.ol-viewport) { |
|||
width: 100% !important; |
|||
height: 100% !important; |
|||
} |
|||
|
|||
:deep(.ol-map) { |
|||
width: 100% !important; |
|||
height: 100% !important; |
|||
} |
|||
|
|||
.info-panel { |
|||
position: absolute; |
|||
top: 100px; |
|||
right: 20px; |
|||
background-color: rgba(255, 255, 255, 0.9); |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); |
|||
z-index: 1000; |
|||
width: 300px; |
|||
max-height: 80vh; |
|||
overflow-y: auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.info-panel__header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 10px 15px; |
|||
border-bottom: 1px solid #eee; |
|||
background-color: #f5f5f5; |
|||
border-radius: 8px 8px 0 0; |
|||
} |
|||
|
|||
.info-panel__title { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.info-panel__close { |
|||
background-color: #ff4d4f; |
|||
color: white; |
|||
border: none; |
|||
border-radius: 50%; |
|||
width: 24px; |
|||
height: 24px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
cursor: pointer; |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
transition: background-color 0.3s ease; |
|||
} |
|||
|
|||
.info-panel__close:hover { |
|||
background-color: #d9363e; |
|||
} |
|||
|
|||
.info-panel__body { |
|||
padding: 15px; |
|||
flex-grow: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.info-panel__empty { |
|||
text-align: center; |
|||
color: #888; |
|||
padding: 20px; |
|||
} |
|||
|
|||
.info-panel__name { |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
margin-bottom: 5px; |
|||
color: #555; |
|||
} |
|||
|
|||
.info-panel__coord { |
|||
font-size: 14px; |
|||
color: #666; |
|||
} |
|||
|
|||
/* 顶部面板样式 */ |
|||
.top-panel { |
|||
position: absolute; |
|||
top: 12px; |
|||
left: 12px; |
|||
right: 12px; |
|||
z-index: 1000; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
gap: 12px; |
|||
padding: 10px 12px; |
|||
flex-wrap: wrap; |
|||
height: 100px; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.top-panel__left { |
|||
flex: 0 0 260px; |
|||
background: rgba(255, 255, 255, 0.7); |
|||
border: 1px solid rgba(0, 0, 0, 0.06); |
|||
border-radius: 10px; |
|||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.search-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
padding: 8px; |
|||
} |
|||
|
|||
.search-type { |
|||
flex: 0 0 120px; |
|||
} |
|||
.search-input { |
|||
width: 220px; |
|||
} |
|||
|
|||
.top-panel__center { |
|||
flex: 1 1 auto; |
|||
display: grid; |
|||
grid-template-columns: repeat(4, minmax(0, 1fr)); |
|||
gap: 12px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.data_item { |
|||
background: rgba(255, 255, 255, 0.85); |
|||
border: 1px solid rgba(0, 0, 0, 0.06); |
|||
border-radius: 8px; |
|||
padding: 8px 12px; |
|||
min-height: 56px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.data_item__title { |
|||
font-size: 12px; |
|||
color: #909399; |
|||
} |
|||
|
|||
.data_item__value { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
color: #303133; |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
.data_item__unit { |
|||
font-size: 12px; |
|||
color: #909399; |
|||
margin-left: 6px; |
|||
} |
|||
|
|||
.top-panel__right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
flex: 0 0 auto; |
|||
background: rgba(255, 255, 255, 0.7); |
|||
border: 1px solid rgba(0, 0, 0, 0.06); |
|||
padding: 8px 12px; |
|||
border-radius: 8px; |
|||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); |
|||
white-space: nowrap; |
|||
height: 100%; |
|||
} |
|||
|
|||
.legend-title { |
|||
color: #606266; |
|||
} |
|||
|
|||
.normal-legend, |
|||
.alarm1-legend, |
|||
.alarm2-legend { |
|||
padding: 4px 8px; |
|||
border-radius: 999px; |
|||
color: #fff; |
|||
font-size: 12px; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.normal-legend { |
|||
background: #67c23a; |
|||
} |
|||
.alarm1-legend { |
|||
background: #e6a23c; |
|||
} |
|||
.alarm2-legend { |
|||
background: #f56c6c; |
|||
} |
|||
|
|||
@media (max-width: 992px) { |
|||
.top-panel__left { |
|||
flex: 1 1 100%; |
|||
} |
|||
.top-panel__center { |
|||
flex: 1 1 100%; |
|||
grid-template-columns: repeat(2, minmax(0, 1fr)); |
|||
} |
|||
.top-panel__right { |
|||
flex: 1 1 100%; |
|||
justify-content: flex-start; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 600px) { |
|||
.top-panel__center { |
|||
grid-template-columns: 1fr; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,526 @@ |
|||
<template> |
|||
<div class="trajectory-controls" v-if="showControls"> |
|||
<!-- 主控制面板 --> |
|||
<div class="control-panel"> |
|||
<!-- 播放控制按钮区域 --> |
|||
<div class="play-controls"> |
|||
<el-button |
|||
:type="playState.isPlaying ? 'warning' : 'primary'" |
|||
:icon="playState.isPlaying ? VideoPause : VideoPlay" |
|||
circle |
|||
size="small" |
|||
@click="handlePlayPause" |
|||
/> |
|||
<el-button |
|||
v-if="playState.isPlaying" |
|||
type="danger" |
|||
:icon="SwitchButton" |
|||
circle |
|||
size="small" |
|||
@click="handleStop" |
|||
/> |
|||
</div> |
|||
|
|||
<!-- 时间范围选择器 --> |
|||
<div class="time-range-controls" v-if="!appStore.mobile"> |
|||
<el-button |
|||
:icon="Calendar" |
|||
size="small" |
|||
:type="showTimeRangePicker ? 'primary' : 'default'" |
|||
@click="toggleTimeRangePicker" |
|||
class="time-range-btn" |
|||
> |
|||
时间选择 |
|||
</el-button> |
|||
</div> |
|||
<!-- 可折叠的时间范围选择器 --> |
|||
<div v-if="showTimeRangePicker && !appStore.mobile" class="time-range-picker"> |
|||
<el-date-picker |
|||
v-model="timeRange" |
|||
type="datetimerange" |
|||
range-separator="至" |
|||
start-placeholder="开始时间" |
|||
end-placeholder="结束时间" |
|||
format="MM-DD HH:mm" |
|||
value-format="x" |
|||
size="small" |
|||
@change="handleTimeRangeChange" |
|||
:clearable="false" |
|||
:shortcuts="shortcuts" |
|||
:unlink-panels="true" |
|||
:editable="false" |
|||
/> |
|||
</div> |
|||
<div v-if="appStore.mobile" class="time-range-picker"> |
|||
<el-button |
|||
@click=" |
|||
handleTimeRangeChange([dayjs().subtract(5, 'minute').valueOf(), dayjs().valueOf()]) |
|||
" |
|||
>近5分钟</el-button |
|||
> |
|||
<el-button |
|||
@click=" |
|||
handleTimeRangeChange([dayjs().subtract(10, 'minute').valueOf(), dayjs().valueOf()]) |
|||
" |
|||
>近10分钟</el-button |
|||
> |
|||
</div> |
|||
<!-- 播放速度选择器 --> |
|||
<div class="speed-controls"> |
|||
<el-select |
|||
size="default" |
|||
v-model="currentSpeed" |
|||
style="width: 80px" |
|||
@change="handleSpeedChange" |
|||
> |
|||
<el-option |
|||
v-for="speed in speedOptions" |
|||
:key="speed.value" |
|||
:label="speed.label" |
|||
:value="speed.value" |
|||
/> |
|||
</el-select> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 时间轴 --> |
|||
<div class="timeline-container"> |
|||
<div class="time-display"> |
|||
{{ formatTime(playState.currentTime) }} |
|||
</div> |
|||
<div class="slider-container"> |
|||
<el-slider |
|||
v-model="currentProgress" |
|||
:min="playState.startTime || 0" |
|||
:max="playState.endTime || Date.now()" |
|||
:show-tooltip="false" |
|||
@change="handleTimeChange" |
|||
size="small" |
|||
/> |
|||
</div> |
|||
<div class="time-display"> |
|||
{{ formatTime(playState.endTime || Date.now()) }} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { ref, watch, computed } from 'vue' |
|||
import dayjs from 'dayjs' |
|||
import type { TrajectoryPlayState } from './types/map.types' |
|||
import { VideoPlay, VideoPause, SwitchButton, Calendar } from '@element-plus/icons-vue' |
|||
import { useAppStore } from '@/store/modules/app' |
|||
interface Props { |
|||
/** 是否显示控制面板 */ |
|||
showControls?: boolean |
|||
/** 播放状态 */ |
|||
playState?: TrajectoryPlayState |
|||
} |
|||
|
|||
interface Emits { |
|||
(e: 'play'): void |
|||
(e: 'pause'): void |
|||
(e: 'stop'): void |
|||
(e: 'speed-change', speed: number): void |
|||
(e: 'time-change', timestamp: number): void |
|||
(e: 'time-range-change', range: { startTime: number; endTime: number }): void |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
showControls: true, |
|||
playState: () => ({ |
|||
isPlaying: false, |
|||
currentTime: dayjs().subtract(1, 'day').valueOf(), |
|||
speed: 1, |
|||
startTime: dayjs().subtract(1, 'day').valueOf(), |
|||
endTime: dayjs().valueOf() |
|||
}) |
|||
}) |
|||
|
|||
const emit = defineEmits<Emits>() |
|||
|
|||
const appStore = useAppStore() |
|||
|
|||
// 播放速度选项 |
|||
const speedOptions = [ |
|||
{ label: '0.5x', value: 0.5 }, |
|||
{ label: '1x', value: 1 }, |
|||
{ label: '2x', value: 2 }, |
|||
{ label: '4x', value: 4 }, |
|||
{ label: '8x', value: 8 } |
|||
] |
|||
|
|||
// 响应式状态 |
|||
const showTimeRangePicker = ref(false) |
|||
const currentSpeed = ref(props.playState.speed) |
|||
const timeRange = ref<[number, number]>([ |
|||
props.playState.startTime || dayjs().subtract(1, 'day').valueOf(), |
|||
props.playState.endTime || dayjs().valueOf() |
|||
]) |
|||
|
|||
// 时间选择快捷项 |
|||
const shortcuts = [ |
|||
{ |
|||
text: '最近1小时', |
|||
value: () => { |
|||
const end = dayjs().valueOf() |
|||
const start = dayjs(end).subtract(1, 'hour').valueOf() |
|||
return [start, end] |
|||
} |
|||
}, |
|||
{ |
|||
text: '最近3小时', |
|||
value: () => { |
|||
const end = dayjs().valueOf() |
|||
const start = dayjs(end).subtract(3, 'hour').valueOf() |
|||
return [start, end] |
|||
} |
|||
}, |
|||
{ |
|||
text: '最近6小时', |
|||
value: () => { |
|||
const end = dayjs().valueOf() |
|||
const start = dayjs(end).subtract(6, 'hour').valueOf() |
|||
return [start, end] |
|||
} |
|||
}, |
|||
{ |
|||
text: '最近24小时', |
|||
value: () => { |
|||
const end = dayjs().valueOf() |
|||
const start = dayjs(end).subtract(24, 'hour').valueOf() |
|||
return [start, end] |
|||
} |
|||
}, |
|||
{ |
|||
text: '最近7天', |
|||
value: () => { |
|||
const end = dayjs().valueOf() |
|||
const start = dayjs(end).subtract(7, 'day').valueOf() |
|||
return [start, end] |
|||
} |
|||
} |
|||
] |
|||
|
|||
// 当前播放进度(用于时间轴滑块) |
|||
const currentProgress = computed({ |
|||
get: () => props.playState.currentTime, |
|||
set: (value: number) => { |
|||
emit('time-change', value) |
|||
} |
|||
}) |
|||
|
|||
// 事件处理函数 |
|||
const handlePlayPause = () => { |
|||
if (props.playState.isPlaying) { |
|||
emit('pause') |
|||
} else { |
|||
emit('play') |
|||
} |
|||
} |
|||
|
|||
const handleStop = () => { |
|||
emit('stop') |
|||
} |
|||
|
|||
const handleSpeedChange = (speed: number) => { |
|||
currentSpeed.value = speed |
|||
emit('speed-change', speed) |
|||
} |
|||
|
|||
const handleTimeChange = (timestamp: number) => { |
|||
emit('time-change', timestamp) |
|||
} |
|||
|
|||
const handleTimeRangeChange = (range: [number, number] | null) => { |
|||
if (range && range.length === 2) { |
|||
timeRange.value = range |
|||
emit('time-range-change', { |
|||
startTime: range[0], |
|||
endTime: range[1] |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const toggleTimeRangePicker = () => { |
|||
showTimeRangePicker.value = !showTimeRangePicker.value |
|||
} |
|||
|
|||
// 时间格式化函数 |
|||
const formatTime = (timestamp: number): string => { |
|||
return dayjs(timestamp).format('MM-DD HH:mm:ss') |
|||
} |
|||
|
|||
// 监听播放状态变化,同步内部状态 |
|||
watch( |
|||
() => props.playState.speed, |
|||
(newSpeed) => { |
|||
currentSpeed.value = newSpeed |
|||
} |
|||
) |
|||
|
|||
watch( |
|||
() => [props.playState.startTime, props.playState.endTime], |
|||
([startTime, endTime]) => { |
|||
if (startTime && endTime) { |
|||
timeRange.value = [startTime, endTime] |
|||
} |
|||
}, |
|||
{ deep: true } |
|||
) |
|||
</script> |
|||
<style scoped> |
|||
.trajectory-controls { |
|||
position: absolute; |
|||
bottom: 50px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
background: rgba(255, 255, 255, 0.95); |
|||
backdrop-filter: blur(10px); |
|||
border-radius: 12px; |
|||
padding: 10px; |
|||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
|||
border: 1px solid rgba(255, 255, 255, 0.2); |
|||
min-width: 480px; |
|||
z-index: 1000; |
|||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|||
} |
|||
|
|||
/* 暗色主题样式 */ |
|||
.dark .trajectory-controls { |
|||
background: rgba(31, 41, 55, 0.95); |
|||
border: 1px solid rgba(75, 85, 99, 0.3); |
|||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.control-panel { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16px; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.play-controls { |
|||
display: flex; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.time-range-controls .time-range-btn { |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.time-range-controls { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.speed-controls { |
|||
margin-left: auto; |
|||
} |
|||
|
|||
.timeline-container { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.time-display { |
|||
font-size: 12px; |
|||
color: #666; |
|||
font-family: monospace; |
|||
min-width: 80px; |
|||
text-align: center; |
|||
transition: color 0.3s ease; |
|||
} |
|||
|
|||
/* 暗色主题下的时间显示 */ |
|||
.dark .time-display { |
|||
color: #9ca3af; |
|||
} |
|||
|
|||
.slider-container { |
|||
flex: 1; |
|||
} |
|||
|
|||
.time-range-picker { |
|||
padding-top: 4px; |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
/* 响应式设计 */ |
|||
@media (max-width: 768px) { |
|||
.trajectory-controls { |
|||
min-width: 320px; |
|||
left: 10px; |
|||
right: 10px; |
|||
transform: none; |
|||
padding: 12px; |
|||
} |
|||
|
|||
.control-panel { |
|||
gap: 16px; |
|||
} |
|||
|
|||
/* 取消外部快捷按钮后,此块无须特殊处理 */ |
|||
|
|||
.speed-controls { |
|||
margin-left: 0; |
|||
align-self: stretch; |
|||
} |
|||
|
|||
.time-display { |
|||
font-size: 11px; |
|||
min-width: 70px; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 480px) { |
|||
.trajectory-controls { |
|||
min-width: unset; |
|||
width: calc(100% - 20px); |
|||
} |
|||
|
|||
.timeline-container { |
|||
gap: 16px; |
|||
} |
|||
|
|||
.slider-container { |
|||
width: 100%; |
|||
} |
|||
|
|||
.time-display { |
|||
min-width: unset; |
|||
} |
|||
} |
|||
|
|||
/* 自定义滑块样式 */ |
|||
:deep(.el-slider__runway) { |
|||
background-color: #e4e7ed; |
|||
height: 6px; |
|||
transition: background-color 0.3s ease; |
|||
} |
|||
|
|||
:deep(.el-slider__bar) { |
|||
height: 6px; |
|||
transition: background 0.3s ease; |
|||
} |
|||
|
|||
:deep(.el-slider__button) { |
|||
width: 16px; |
|||
height: 16px; |
|||
border: 2px solid #409eff; |
|||
background-color: #fff; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
:deep(.el-slider__button:hover) { |
|||
transform: scale(1.2); |
|||
} |
|||
|
|||
/* 暗色主题下的滑块样式 */ |
|||
.dark :deep(.el-slider__runway) { |
|||
background-color: #374151; |
|||
} |
|||
|
|||
:global(.dark) :deep(.el-slider__button) { |
|||
border: 2px solid #3b82f6; |
|||
background-color: #1f2937; |
|||
} |
|||
|
|||
.dark :deep(.el-slider__button:hover) { |
|||
background-color: #374151; |
|||
} |
|||
|
|||
/* 美化按钮样式 */ |
|||
:deep(.el-button.is-circle) { |
|||
padding: 8px; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
:deep(.el-button--primary.is-circle) { |
|||
border: none; |
|||
} |
|||
|
|||
:deep(.el-button--warning.is-circle) { |
|||
border: none; |
|||
} |
|||
|
|||
:deep(.el-button--danger.is-circle) { |
|||
border: none; |
|||
} |
|||
|
|||
/* 日期选择器和下拉框的暗色主题优化 */ |
|||
.dark :deep(.el-date-editor) { |
|||
background-color: #374151; |
|||
border-color: #4b5563; |
|||
color: #e5e7eb; |
|||
} |
|||
|
|||
.dark :deep(.el-date-editor:hover) { |
|||
border-color: #6b7280; |
|||
} |
|||
|
|||
.dark :deep(.el-date-editor.is-focus) { |
|||
border-color: #3b82f6; |
|||
} |
|||
|
|||
.dark :deep(.el-date-editor .el-input__inner) { |
|||
background-color: transparent; |
|||
color: #e5e7eb; |
|||
} |
|||
|
|||
.dark :deep(.el-date-editor .el-input__inner::placeholder) { |
|||
color: #9ca3af; |
|||
} |
|||
|
|||
.dark :deep(.el-select .el-input__inner) { |
|||
background-color: #374151; |
|||
border-color: #4b5563; |
|||
color: #e5e7eb; |
|||
} |
|||
|
|||
.dark :deep(.el-select .el-input__inner:hover) { |
|||
border-color: #6b7280; |
|||
} |
|||
|
|||
.dark :deep(.el-select .el-input__inner:focus) { |
|||
border-color: #3b82f6; |
|||
} |
|||
|
|||
/* 毛玻璃效果增强 */ |
|||
.trajectory-controls::before { |
|||
content: ''; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: inherit; |
|||
border-radius: inherit; |
|||
backdrop-filter: blur(20px) saturate(180%); |
|||
z-index: -1; |
|||
} |
|||
|
|||
/* 暗色主题下的边框光效 */ |
|||
.dark .trajectory-controls::after { |
|||
content: ''; |
|||
position: absolute; |
|||
top: -1px; |
|||
left: -1px; |
|||
right: -1px; |
|||
bottom: -1px; |
|||
border-radius: inherit; |
|||
z-index: -2; |
|||
opacity: 0; |
|||
transition: opacity 0.3s ease; |
|||
} |
|||
|
|||
.dark .trajectory-controls:hover::after { |
|||
opacity: 1; |
|||
} |
|||
</style> |
@ -0,0 +1,273 @@ |
|||
/** |
|||
* 地图事件处理相关的 composable |
|||
*/ |
|||
import dayjs from 'dayjs' |
|||
import { fromLonLat } from 'ol/proj' |
|||
import { TrajectoryService } from '../services/trajectory.service' |
|||
import { PopupService } from '../services/popup.service' |
|||
|
|||
interface PopupContentGenerator { |
|||
handleTrajectoryPoint: (feature: any) => string |
|||
handleTrajectoryLine: (feature: any) => string |
|||
handleFence: (feature: any) => string |
|||
handleMarker: (feature: any) => string |
|||
} |
|||
|
|||
export const useMapEvents = () => { |
|||
// 创建弹窗内容生成器
|
|||
const createPopupContentGenerator = ( |
|||
trajectoryService: any, |
|||
popupService: any |
|||
): PopupContentGenerator => ({ |
|||
handleTrajectoryPoint: (feature: any): string => { |
|||
const timeText = feature.get('timeText') || '' |
|||
const trajectoryId = feature.get('trajectoryId') || '' |
|||
const timestamp = feature.get('timestamp') |
|||
const deviceName = |
|||
trajectoryService?.getTrajectoryData().find((t) => t.deviceId === trajectoryId)?.name || |
|||
trajectoryId |
|||
|
|||
return ` |
|||
<div style="font-size: 12px; color: #333;"> |
|||
<div style="font-weight: bold; margin-bottom: 4px;">${deviceName}</div> |
|||
<div>时间: ${timeText}</div> |
|||
<div style="font-size: 10px; color: #666;"> |
|||
${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')} |
|||
</div> |
|||
</div> |
|||
` |
|||
}, |
|||
|
|||
handleTrajectoryLine: (feature: any): string => { |
|||
const deviceId = feature.get('deviceId') || '' |
|||
const deviceName = |
|||
trajectoryService?.getTrajectoryData().find((t) => t.deviceId === deviceId)?.name || |
|||
deviceId |
|||
|
|||
return ` |
|||
<div style="font-size: 12px; color: #333;"> |
|||
<div style="font-weight: bold;">${deviceName} - 轨迹路径</div> |
|||
</div> |
|||
` |
|||
}, |
|||
|
|||
handleFence: (feature: any): string => { |
|||
const fenceData = feature.get('fenceData') |
|||
const statusText = |
|||
fenceData.status === 0 ? '正常' : fenceData.status === 1 ? '一级报警' : '二级报警' |
|||
const typeText = fenceData.type === 0 ? '包含' : '排斥' |
|||
|
|||
return ` |
|||
<div style="font-size: 12px; color: #333;"> |
|||
<div style="font-weight: bold; margin-bottom: 4px;">${fenceData.name}</div> |
|||
<div>状态: ${statusText}</div> |
|||
<div>类型: ${typeText}</div> |
|||
<div style="font-size: 10px; color: #666; margin-top: 2px;"> |
|||
${fenceData.remark || '无备注'} |
|||
</div> |
|||
</div> |
|||
` |
|||
}, |
|||
|
|||
handleMarker: (feature: any): string => { |
|||
return popupService?.handlePopupContent(feature) || '' |
|||
} |
|||
}) |
|||
|
|||
/** |
|||
* 设置地图事件监听器 |
|||
*/ |
|||
const setupMapEventListeners = ( |
|||
map: any, |
|||
popupOverlay: any, |
|||
trajectoryService: TrajectoryService | null, |
|||
popupService: PopupService | null, |
|||
opts?: { |
|||
isDrawing?: () => boolean |
|||
onMarkerClick?: (markerData: any) => void |
|||
markerLayer?: any |
|||
refreshMarkerStyles?: () => void |
|||
} |
|||
) => { |
|||
const popupGenerator = createPopupContentGenerator(trajectoryService, popupService) |
|||
|
|||
// 鼠标悬停事件
|
|||
const handlePointerMove = (event: any) => { |
|||
// 绘制围栏时屏蔽 hover 弹窗
|
|||
if (opts?.isDrawing && opts.isDrawing()) { |
|||
hidePopup(popupOverlay) |
|||
return |
|||
} |
|||
const feature = map.forEachFeatureAtPixel(event.pixel, (feature: any) => feature) |
|||
|
|||
if (feature) { |
|||
map.getTargetElement().style.cursor = 'pointer' |
|||
showPopup(event, feature, popupOverlay, popupGenerator) |
|||
} else { |
|||
map.getTargetElement().style.cursor = '' |
|||
hidePopup(popupOverlay) |
|||
} |
|||
} |
|||
|
|||
// 点击事件
|
|||
const handleClick = (event: any) => { |
|||
// 绘制围栏时屏蔽点击处理
|
|||
if (opts?.isDrawing && opts.isDrawing()) { |
|||
return |
|||
} |
|||
const feature = map.forEachFeatureAtPixel(event.pixel, (feature: any) => feature) |
|||
if (feature) { |
|||
handleFeatureClick(feature, map, opts) |
|||
} |
|||
} |
|||
|
|||
// 地图移动结束事件(包括放缩)
|
|||
const handleMoveEnd = () => { |
|||
// OpenLayers的Cluster会自动重新计算聚合,只需要刷新样式
|
|||
if (opts?.markerLayer) { |
|||
opts.markerLayer.changed() |
|||
} |
|||
} |
|||
|
|||
map.on('pointermove', handlePointerMove) |
|||
map.on('click', handleClick) |
|||
map.on('moveend', handleMoveEnd) |
|||
|
|||
return { |
|||
destroy: () => { |
|||
map.un('pointermove', handlePointerMove) |
|||
map.un('click', handleClick) |
|||
map.un('moveend', handleMoveEnd) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 显示弹窗 |
|||
*/ |
|||
const showPopup = ( |
|||
event: any, |
|||
feature: any, |
|||
popupOverlay: any, |
|||
popupGenerator: PopupContentGenerator |
|||
) => { |
|||
if (!popupOverlay) return |
|||
|
|||
const popupElement = popupOverlay.getElement() |
|||
if (!popupElement) return |
|||
|
|||
const featureType = feature.get('type') |
|||
let popupContent = '' |
|||
|
|||
switch (featureType) { |
|||
case 'trajectory-point': |
|||
popupContent = popupGenerator.handleTrajectoryPoint(feature) |
|||
break |
|||
case 'trajectory': |
|||
popupContent = popupGenerator.handleTrajectoryLine(feature) |
|||
break |
|||
case 'fence': |
|||
case 'fence-label': |
|||
popupContent = popupGenerator.handleFence(feature) |
|||
break |
|||
default: |
|||
popupContent = popupGenerator.handleMarker(feature) |
|||
break |
|||
} |
|||
|
|||
popupElement.innerHTML = popupContent |
|||
popupOverlay.setPosition(event.coordinate) |
|||
} |
|||
|
|||
/** |
|||
* 隐藏弹窗 |
|||
*/ |
|||
const hidePopup = (popupOverlay: any) => { |
|||
if (popupOverlay) { |
|||
popupOverlay.setPosition(undefined) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理特征点击事件 |
|||
*/ |
|||
const handleFeatureClick = ( |
|||
feature: any, |
|||
map: any, |
|||
opts?: { onMarkerClick?: (markerData: any) => void } |
|||
) => { |
|||
const featureType = feature.get('type') |
|||
|
|||
// 处理围栏点击
|
|||
if (featureType === 'fence' || featureType === 'fence-label') { |
|||
const fenceData = feature.get('fenceData') |
|||
if (fenceData) { |
|||
console.log('围栏点击:', fenceData) |
|||
// 可以在这里添加围栏点击的自定义处理逻辑
|
|||
} |
|||
return |
|||
} |
|||
|
|||
// 处理标记点击
|
|||
handleMarkerClick(feature, map, opts) |
|||
} |
|||
|
|||
/** |
|||
* 处理标记点击事件 |
|||
*/ |
|||
const handleMarkerClick = ( |
|||
feature: any, |
|||
map: any, |
|||
opts?: { onMarkerClick?: (markerData: any) => void } |
|||
) => { |
|||
const markerData = feature.get('markerData') |
|||
const features = feature.get('features') |
|||
|
|||
if (features && features.length > 1) { |
|||
// 处理聚合标记点击
|
|||
handleClusterClick(features, map) |
|||
} else if (features && features.length === 1) { |
|||
// 处理聚合中的单个标记点击
|
|||
const singleMarkerData = features[0].get('markerData') |
|||
if (singleMarkerData) { |
|||
animateToCoordinate(singleMarkerData.coordinates, map, 15) |
|||
opts?.onMarkerClick?.(singleMarkerData) |
|||
} |
|||
} else if (markerData) { |
|||
// 处理非聚合的单个标记点击
|
|||
animateToCoordinate(markerData.coordinates, map, 15) |
|||
opts?.onMarkerClick?.(markerData) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理聚合标记点击 |
|||
*/ |
|||
const handleClusterClick = (features: any[], map: any) => { |
|||
// 计算聚合标记的中心点
|
|||
const coordinates = features.map((f: any) => f.get('markerData').coordinates) |
|||
const centerLon = |
|||
coordinates.reduce((sum: number, coord: any) => sum + coord[0], 0) / coordinates.length |
|||
const centerLat = |
|||
coordinates.reduce((sum: number, coord: any) => sum + coord[1], 0) / coordinates.length |
|||
|
|||
animateToCoordinate([centerLon, centerLat], map, 12) |
|||
} |
|||
|
|||
/** |
|||
* 动画移动到指定坐标 |
|||
*/ |
|||
const animateToCoordinate = (coordinates: [number, number], map: any, zoom: number) => { |
|||
const view = map.getView() |
|||
|
|||
view.animate({ |
|||
center: fromLonLat(coordinates), |
|||
zoom: Math.max(view.getZoom() || 10, zoom), |
|||
duration: 1000 |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
setupMapEventListeners |
|||
} |
|||
} |
@ -0,0 +1,297 @@ |
|||
/** |
|||
* 地图服务管理相关的 composable |
|||
*/ |
|||
import { ref, onUnmounted, reactive } from 'vue' |
|||
import type { MapProps } from '../types/map.types' |
|||
import { MapService } from '../services/map.service' |
|||
import { MarkerService } from '../services/marker.service' |
|||
import { AnimationService } from '../services/animation.service' |
|||
import { PopupService } from '../services/popup.service' |
|||
import { TrajectoryService } from '../services/trajectory.service' |
|||
import { FenceService } from '../services/fence.service' |
|||
import { FenceDrawService } from '../services/fence-draw.service' |
|||
|
|||
interface ServiceInstances { |
|||
mapService: MapService | null |
|||
markerService: MarkerService | null |
|||
animationService: AnimationService | null |
|||
popupService: PopupService | null |
|||
trajectoryService: TrajectoryService | null |
|||
fenceService: FenceService | null |
|||
fenceDrawService: FenceDrawService | null |
|||
} |
|||
|
|||
interface LayerRefs { |
|||
markerLayer: any |
|||
rippleLayer: any |
|||
trajectoryLayer: any |
|||
fenceLayer: any |
|||
} |
|||
|
|||
export const useMapServices = () => { |
|||
// 服务实例状态
|
|||
const services = reactive<ServiceInstances>({ |
|||
mapService: null, |
|||
markerService: null, |
|||
animationService: null, |
|||
popupService: null, |
|||
trajectoryService: null, |
|||
fenceService: null, |
|||
fenceDrawService: null |
|||
}) |
|||
|
|||
// 图层引用状态
|
|||
const layerRefs = ref<LayerRefs | null>(null) |
|||
|
|||
/** |
|||
* 初始化所有服务 |
|||
*/ |
|||
const initializeServices = (map?: any) => { |
|||
// 就地更新,保持对 services 的引用不变,避免外部拿到旧引用
|
|||
services.mapService = new MapService() |
|||
services.markerService = new MarkerService() |
|||
services.animationService = new AnimationService() |
|||
services.popupService = new PopupService() |
|||
services.trajectoryService = new TrajectoryService() |
|||
services.fenceService = new FenceService() |
|||
services.fenceDrawService = new FenceDrawService() |
|||
|
|||
// 如果提供了地图实例,设置给相关服务
|
|||
if (map) { |
|||
services.markerService.setMap(map) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 初始化地图和图层 |
|||
*/ |
|||
const initializeMapAndLayers = ( |
|||
mapInstance: { map: any; popupOverlay: any }, |
|||
props: MapProps |
|||
) => { |
|||
// 初始化地图
|
|||
const map = mapInstance.map |
|||
const popupOverlay = mapInstance.popupOverlay |
|||
|
|||
// 重新初始化服务,确保markerService有地图实例
|
|||
initializeServices(map) |
|||
|
|||
if ( |
|||
!services.markerService || |
|||
!services.animationService || |
|||
!services.trajectoryService || |
|||
!services.fenceService || |
|||
!services.fenceDrawService |
|||
) { |
|||
throw new Error('Services not initialized') |
|||
} |
|||
|
|||
// 创建各种图层
|
|||
const markerLayer = services.markerService.createMarkerLayer(props, map) |
|||
const rippleLayer = services.animationService.createRippleLayer( |
|||
props.markers || [], |
|||
map, |
|||
props.enableCluster |
|||
) |
|||
const trajectoryLayer = services.trajectoryService.createTrajectoryLayer(map) |
|||
const fenceLayer = services.fenceService.createFenceLayer(props.fences || [], map) |
|||
|
|||
// // 添加图层到地图
|
|||
map.addLayer(markerLayer) |
|||
map.addLayer(rippleLayer) |
|||
map.addLayer(trajectoryLayer) |
|||
map.addLayer(fenceLayer) |
|||
|
|||
// 初始化围栏绘制服务
|
|||
services.fenceDrawService.init(map) |
|||
|
|||
// 存储图层引用
|
|||
layerRefs.value = { |
|||
markerLayer, |
|||
rippleLayer, |
|||
trajectoryLayer, |
|||
fenceLayer |
|||
} |
|||
return { |
|||
map, |
|||
popupOverlay |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置标记显示状态 |
|||
*/ |
|||
const setMarkersVisible = (visible: boolean) => { |
|||
if (!layerRefs.value || !services.animationService) return |
|||
|
|||
const { markerLayer, rippleLayer } = layerRefs.value |
|||
|
|||
if (visible) { |
|||
markerLayer.setVisible(true) |
|||
rippleLayer.setVisible(true) |
|||
services.animationService.startAnimation() |
|||
} else { |
|||
markerLayer.setVisible(false) |
|||
rippleLayer.setVisible(false) |
|||
services.animationService.stopAnimation() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹显示状态 |
|||
*/ |
|||
const setTrajectoriesVisible = (visible: boolean, markers: any[] = []) => { |
|||
if (!services.trajectoryService) return |
|||
|
|||
if (visible) { |
|||
services.trajectoryService.setTrajectoryData(markers) |
|||
services.trajectoryService.showTrajectories() |
|||
} else { |
|||
services.trajectoryService.hideTrajectories() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置围栏显示状态 |
|||
*/ |
|||
const setFencesVisible = (visible: boolean) => { |
|||
if (!services.fenceService) return |
|||
|
|||
if (visible) { |
|||
services.fenceService.showFences() |
|||
} else { |
|||
services.fenceService.hideFences() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 开始/停止围栏绘制 |
|||
*/ |
|||
const toggleFenceDrawing = ( |
|||
isDrawing: boolean, |
|||
onComplete?: (coordinates: [number, number][]) => void |
|||
) => { |
|||
if (!services.fenceDrawService) return |
|||
|
|||
if (isDrawing && onComplete) { |
|||
services.fenceDrawService.startDrawing(onComplete) |
|||
} else { |
|||
services.fenceDrawService.stopDrawing() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清理围栏绘制图层 |
|||
*/ |
|||
const clearFenceDrawLayer = () => { |
|||
if (services.fenceDrawService) { |
|||
services.fenceDrawService.clearDrawLayer() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 更新标记数据 |
|||
*/ |
|||
const updateMarkers = (markers: any[], currentProps?: any) => { |
|||
if ( |
|||
services.markerService && |
|||
services.animationService && |
|||
layerRefs.value?.markerLayer && |
|||
layerRefs.value?.rippleLayer |
|||
) { |
|||
const map = services.mapService?.getMap() |
|||
const enableCluster = currentProps?.enableCluster ?? true |
|||
if (map) { |
|||
// 从地图中移除旧的marker layer
|
|||
map.removeLayer(layerRefs.value.markerLayer) |
|||
map.removeLayer(layerRefs.value.rippleLayer) |
|||
|
|||
// 更新marker service(这会创建新的layer)
|
|||
services.markerService.updateMarkers(markers) |
|||
|
|||
// 重新创建波纹图层
|
|||
const newRippleLayer = services.animationService.createRippleLayer( |
|||
markers, |
|||
map, |
|||
enableCluster |
|||
) |
|||
|
|||
// 获取新的layer并添加到地图
|
|||
const newMarkerLayer = services.markerService.getMarkerLayer() |
|||
if (newMarkerLayer && newRippleLayer) { |
|||
// 确保markerService有最新的地图实例
|
|||
services.markerService.setMap(map) |
|||
map.addLayer(newMarkerLayer) |
|||
map.addLayer(newRippleLayer) |
|||
layerRefs.value.markerLayer = newMarkerLayer |
|||
layerRefs.value.rippleLayer = newRippleLayer |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新标记样式 |
|||
*/ |
|||
const refreshMarkerStyles = () => { |
|||
if (services.markerService) { |
|||
services.markerService.refreshStyles() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 销毁所有服务 |
|||
*/ |
|||
const destroyServices = () => { |
|||
// 销毁有 destroy 方法的服务
|
|||
const servicesToDestroy = [ |
|||
services.mapService, |
|||
services.markerService, |
|||
services.animationService, |
|||
services.trajectoryService, |
|||
services.fenceService, |
|||
services.fenceDrawService |
|||
] |
|||
|
|||
servicesToDestroy.forEach((service) => { |
|||
if (service && typeof service.destroy === 'function') { |
|||
service.destroy() |
|||
} |
|||
}) |
|||
|
|||
// 重置服务字段(保持 reactive 对象引用不变)
|
|||
services.mapService = null |
|||
services.markerService = null |
|||
services.animationService = null |
|||
services.popupService = null |
|||
services.trajectoryService = null |
|||
services.fenceService = null |
|||
services.fenceDrawService = null |
|||
|
|||
// 重置图层引用
|
|||
layerRefs.value = null |
|||
} |
|||
|
|||
// 组件卸载时自动清理
|
|||
onUnmounted(() => { |
|||
destroyServices() |
|||
}) |
|||
|
|||
return { |
|||
// 状态
|
|||
services, |
|||
layerRefs, |
|||
|
|||
// 方法
|
|||
initializeServices, |
|||
initializeMapAndLayers, |
|||
setMarkersVisible, |
|||
setTrajectoriesVisible, |
|||
setFencesVisible, |
|||
toggleFenceDrawing, |
|||
clearFenceDrawLayer, |
|||
updateMarkers, |
|||
refreshMarkerStyles, |
|||
destroyServices |
|||
} |
|||
} |
@ -0,0 +1,139 @@ |
|||
/** |
|||
* 地图状态监听相关的 composable |
|||
*/ |
|||
import { watch, type Ref } from 'vue' |
|||
|
|||
interface WatchOptions { |
|||
showMarkers: Ref<boolean> |
|||
showTrajectories: Ref<boolean> |
|||
showFences: Ref<boolean> |
|||
showDrawFences: Ref<boolean> |
|||
setMarkersVisible: (visible: boolean) => void |
|||
setTrajectoriesVisible: (visible: boolean, markers?: any[]) => void |
|||
setFencesVisible: (visible: boolean) => void |
|||
toggleFenceDrawing: ( |
|||
isDrawing: boolean, |
|||
onComplete?: (coordinates: [number, number][]) => void |
|||
) => void |
|||
updateMarkers: (markers: any[]) => void |
|||
markers: any[] |
|||
} |
|||
|
|||
export const useMapWatchers = (options: WatchOptions) => { |
|||
const { |
|||
showMarkers, |
|||
showTrajectories, |
|||
showFences, |
|||
showDrawFences, |
|||
setMarkersVisible, |
|||
setTrajectoriesVisible, |
|||
setFencesVisible, |
|||
toggleFenceDrawing, |
|||
updateMarkers, |
|||
markers |
|||
} = options |
|||
|
|||
/** |
|||
* 设置标记显示状态监听器 |
|||
*/ |
|||
const setupMarkersWatcher = () => { |
|||
return watch(showMarkers, (show) => { |
|||
if (show) { |
|||
// 显示标记时,隐藏轨迹
|
|||
if (showTrajectories.value) { |
|||
showTrajectories.value = false |
|||
} |
|||
} |
|||
setMarkersVisible(show) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹显示状态监听器 |
|||
*/ |
|||
const setupTrajectoriesWatcher = () => { |
|||
return watch(showTrajectories, (show) => { |
|||
if (show) { |
|||
// 显示轨迹时,隐藏标记
|
|||
if (showMarkers.value) { |
|||
showMarkers.value = false |
|||
} |
|||
setTrajectoriesVisible(true, markers) |
|||
} else { |
|||
setTrajectoriesVisible(false) |
|||
// 隐藏轨迹时,显示标记
|
|||
if (!showMarkers.value) { |
|||
showMarkers.value = true |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置围栏显示状态监听器 |
|||
*/ |
|||
const setupFencesWatcher = () => { |
|||
return watch(showFences, (show) => { |
|||
setFencesVisible(show) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置围栏绘制状态监听器 |
|||
*/ |
|||
const setupDrawFencesWatcher = () => { |
|||
return watch(showDrawFences, (isDrawing) => { |
|||
if (!isDrawing) { |
|||
// 当关闭绘制时,停止绘制操作
|
|||
toggleFenceDrawing(false) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置标记数据变化监听器 |
|||
*/ |
|||
const setupMarkersDataWatcher = () => { |
|||
return watch( |
|||
markers, |
|||
(newMarkers) => { |
|||
if (newMarkers && newMarkers.length > 0) { |
|||
console.log('Markers data changed, updating markers:', newMarkers.length) |
|||
updateMarkers(newMarkers) |
|||
} |
|||
}, |
|||
{ deep: true, immediate: false } |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 初始化所有监听器 |
|||
*/ |
|||
const setupAllWatchers = () => { |
|||
const watchers = [ |
|||
setupMarkersWatcher(), |
|||
setupTrajectoriesWatcher(), |
|||
setupFencesWatcher(), |
|||
setupDrawFencesWatcher(), |
|||
setupMarkersDataWatcher() |
|||
] |
|||
|
|||
// 返回清理函数
|
|||
return () => { |
|||
watchers.forEach((stopWatcher) => { |
|||
if (typeof stopWatcher === 'function') { |
|||
stopWatcher() |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
return { |
|||
setupMarkersWatcher, |
|||
setupTrajectoriesWatcher, |
|||
setupFencesWatcher, |
|||
setupDrawFencesWatcher, |
|||
setupMarkersDataWatcher, |
|||
setupAllWatchers |
|||
} |
|||
} |
@ -0,0 +1,147 @@ |
|||
/** |
|||
* 轨迹控制相关的 composable |
|||
*/ |
|||
import { ref, watch } from 'vue' |
|||
import dayjs from 'dayjs' |
|||
import type { TrajectoryPlayState } from '../types/map.types' |
|||
import { TrajectoryService } from '../services/trajectory.service' |
|||
|
|||
export const useTrajectoryControls = () => { |
|||
// 轨迹播放状态
|
|||
const trajectoryPlayState = ref<TrajectoryPlayState>({ |
|||
isPlaying: false, |
|||
currentTime: dayjs().subtract(1, 'day').valueOf(), |
|||
speed: 1, |
|||
startTime: dayjs().subtract(1, 'day').valueOf(), |
|||
endTime: dayjs().valueOf() |
|||
}) |
|||
|
|||
// 轨迹播放定时器
|
|||
const trajectoryPlayTimer = ref<number | null>(null) |
|||
|
|||
/** |
|||
* 播放轨迹 |
|||
*/ |
|||
const playTrajectory = () => { |
|||
if (trajectoryPlayTimer.value) { |
|||
window.clearInterval(trajectoryPlayTimer.value) |
|||
} |
|||
|
|||
trajectoryPlayTimer.value = window.setInterval(() => { |
|||
trajectoryPlayState.value.currentTime += 1000 * trajectoryPlayState.value.speed |
|||
trajectoryPlayState.value.isPlaying = true |
|||
}, 1000) |
|||
} |
|||
|
|||
/** |
|||
* 暂停轨迹 |
|||
*/ |
|||
const pauseTrajectory = () => { |
|||
if (trajectoryPlayTimer.value) { |
|||
window.clearInterval(trajectoryPlayTimer.value) |
|||
trajectoryPlayTimer.value = null |
|||
trajectoryPlayState.value.isPlaying = false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止轨迹 |
|||
*/ |
|||
const stopTrajectory = () => { |
|||
if (trajectoryPlayTimer.value) { |
|||
window.clearInterval(trajectoryPlayTimer.value) |
|||
trajectoryPlayTimer.value = null |
|||
trajectoryPlayState.value.isPlaying = false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹播放速度 |
|||
*/ |
|||
const setTrajectorySpeed = (speed: number) => { |
|||
trajectoryPlayState.value.speed = speed |
|||
|
|||
// 如果正在播放,重启定时器以应用新速度
|
|||
if (trajectoryPlayTimer.value) { |
|||
window.clearInterval(trajectoryPlayTimer.value) |
|||
trajectoryPlayTimer.value = window.setInterval(() => { |
|||
trajectoryPlayState.value.currentTime += 1000 * trajectoryPlayState.value.speed |
|||
}, 1000) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹播放时间 |
|||
*/ |
|||
const setTrajectoryTime = (timestamp: number) => { |
|||
trajectoryPlayState.value.currentTime = timestamp |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹时间范围 |
|||
*/ |
|||
const setTrajectoryTimeRange = (range: { startTime: number; endTime: number }) => { |
|||
// 停止当前播放
|
|||
if (trajectoryPlayTimer.value) { |
|||
window.clearInterval(trajectoryPlayTimer.value) |
|||
trajectoryPlayTimer.value = null |
|||
} |
|||
|
|||
// 更新轨迹播放状态的时间范围
|
|||
trajectoryPlayState.value = { |
|||
...trajectoryPlayState.value, |
|||
isPlaying: false, |
|||
currentTime: range.startTime, |
|||
startTime: range.startTime, |
|||
endTime: range.endTime |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹播放状态监听器 |
|||
*/ |
|||
const setupTrajectoryWatcher = ( |
|||
trajectoryService: TrajectoryService | null, |
|||
showTrajectories: { value: boolean } |
|||
) => { |
|||
// 监听轨迹播放状态变化
|
|||
const stopPlayStateWatcher = watch( |
|||
() => trajectoryPlayState.value, |
|||
(newState) => { |
|||
if (trajectoryService && showTrajectories.value) { |
|||
trajectoryService.updateByPlayState(newState) |
|||
} |
|||
}, |
|||
{ deep: true } |
|||
) |
|||
|
|||
return { |
|||
stopPlayStateWatcher |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清理轨迹控制器 |
|||
*/ |
|||
const cleanup = () => { |
|||
if (trajectoryPlayTimer.value) { |
|||
window.clearInterval(trajectoryPlayTimer.value) |
|||
trajectoryPlayTimer.value = null |
|||
} |
|||
} |
|||
|
|||
return { |
|||
// 状态
|
|||
trajectoryPlayState, |
|||
|
|||
// 方法
|
|||
playTrajectory, |
|||
pauseTrajectory, |
|||
stopTrajectory, |
|||
setTrajectorySpeed, |
|||
setTrajectoryTime, |
|||
setTrajectoryTimeRange, |
|||
setupTrajectoryWatcher, |
|||
cleanup |
|||
} |
|||
} |
@ -0,0 +1,69 @@ |
|||
/** |
|||
* 地图组件常量配置 |
|||
*/ |
|||
import type { StatusDictItem, FenceData } from '../types/map.types' |
|||
|
|||
// 状态字典配置
|
|||
export const STATUS_DICT = { |
|||
online: [ |
|||
{ value: '0', label: '离线', cssClass: '#909399' }, |
|||
{ value: '1', label: '在线', cssClass: '#67c23a' } |
|||
] as StatusDictItem[], |
|||
gas: [ |
|||
{ value: '0', label: '正常', cssClass: '#67c23a' }, |
|||
{ value: '1', label: '一级气体报警', cssClass: '#e6a23c' }, |
|||
{ value: '2', label: '二级气体告警', cssClass: '#f56c6c' } |
|||
] as StatusDictItem[], |
|||
battery: [ |
|||
{ value: '0', label: '正常', cssClass: '#67c23a' }, |
|||
{ value: '1', label: '一级低电量报警', cssClass: '#e6a23c' }, |
|||
{ value: '2', label: '二级低电量报警', cssClass: '#f56c6c' } |
|||
] as StatusDictItem[], |
|||
fence: [ |
|||
{ value: '0', label: '正常', cssClass: '#67c23a' }, |
|||
{ value: '1', label: '一级围栏报警', cssClass: '#e6a23c' }, |
|||
{ value: '2', label: '二级围栏报警', cssClass: '#f56c6c' } |
|||
] as StatusDictItem[] |
|||
} |
|||
|
|||
// 状态优先级定义 (数字越小优先级越高)
|
|||
export const STATUS_PRIORITY = { |
|||
gas_2: 1, |
|||
gas_1: 2, |
|||
battery_2: 3, |
|||
battery_1: 4, |
|||
fence_2: 5, |
|||
fence_1: 6, |
|||
offline: 7, |
|||
normal: 8 |
|||
} as const |
|||
|
|||
// 状态顺序数组
|
|||
export const STATUS_ORDER = Object.keys(STATUS_PRIORITY) as Array<keyof typeof STATUS_PRIORITY> |
|||
|
|||
// 默认标记数据
|
|||
export const DEFAULT_MARKERS = [] |
|||
|
|||
// 地图默认配置
|
|||
export const MAP_DEFAULTS = { |
|||
tileUrl: 'http://qtbj.icpcdev.site/roadmap/{z}/{x}/{y}.png', |
|||
center: [116.3912757, 39.906217] as [number, number], |
|||
zoom: 10, |
|||
maxZoom: 18, |
|||
minZoom: 0, |
|||
enableCluster: true, |
|||
clusterDistance: 0 |
|||
} |
|||
|
|||
// 动画配置
|
|||
export const ANIMATION_CONFIG = { |
|||
duration: 3, // 动画周期(秒)
|
|||
rippleCount: 5, // 波纹圈数量
|
|||
phaseOffset: 0.6, // 波纹圈错开时间(秒)
|
|||
targetFPS: 60, // 目标帧率
|
|||
clusterThreshold: 12, // 聚合阈值
|
|||
minRadius: 6, // 最小半径
|
|||
maxRadius: 31, // 最大半径
|
|||
minOpacity: 0.05 // 最小透明度阈值
|
|||
} |
|||
export const DEFAULT_FENCES = [] as FenceData[] |
@ -0,0 +1,162 @@ |
|||
/** |
|||
* 动画服务类 |
|||
*/ |
|||
import { Vector as VectorLayer } from 'ol/layer' |
|||
import { Vector as VectorSource } from 'ol/source' |
|||
import { Feature } from 'ol' |
|||
import { Point } from 'ol/geom' |
|||
import { Style, Circle, Fill, Stroke } from 'ol/style' |
|||
import { fromLonLat } from 'ol/proj' |
|||
import type { MarkerData } from '../types/map.types' |
|||
import { getHighestPriorityStatus, getStatusColor } from '../utils/map.utils' |
|||
import { ANIMATION_CONFIG } from '../constants/map.constants' |
|||
|
|||
export class AnimationService { |
|||
private rippleLayer: VectorLayer<VectorSource> | null = null |
|||
private animationTimer: number | null = null |
|||
private map: any = null |
|||
private enableCluster: boolean = true |
|||
|
|||
/** |
|||
* 创建波纹图层 |
|||
*/ |
|||
createRippleLayer( |
|||
markers: MarkerData[], |
|||
map: any, |
|||
enableCluster: boolean = true |
|||
): VectorLayer<VectorSource> { |
|||
this.map = map |
|||
this.enableCluster = enableCluster |
|||
const source = new VectorSource() |
|||
|
|||
// 为每个标记添加波纹效果
|
|||
markers.forEach((marker) => { |
|||
const feature = new Feature({ |
|||
geometry: new Point(fromLonLat(marker.coordinates)), |
|||
markerData: marker |
|||
}) |
|||
|
|||
const status = getHighestPriorityStatus(marker) |
|||
const color = getStatusColor(status) |
|||
|
|||
// 设置动画开始时间
|
|||
feature.set('animationStart', Date.now()) |
|||
feature.set('rippleColor', color) |
|||
|
|||
source.addFeature(feature) |
|||
}) |
|||
|
|||
this.rippleLayer = new VectorLayer({ |
|||
source: source, |
|||
style: (feature) => { |
|||
// 检查当前缩放级别,如果缩放级别较低(聚合状态),不显示波纹
|
|||
const currentZoom = this.map?.getView().getZoom() || 0 |
|||
|
|||
// 如果启用了聚合且zoom级别较低,不显示波纹
|
|||
if (this.enableCluster && currentZoom < ANIMATION_CONFIG.clusterThreshold) { |
|||
return [] // 不显示波纹
|
|||
} |
|||
|
|||
const startTime = feature.get('animationStart') |
|||
const color = feature.get('rippleColor') |
|||
const elapsed = (Date.now() - startTime) / 1000 // 秒
|
|||
|
|||
// 创建多个波纹圈
|
|||
const styles: Style[] = [] |
|||
|
|||
for (let i = 0; i < ANIMATION_CONFIG.rippleCount; i++) { |
|||
const phase = (elapsed + i * ANIMATION_CONFIG.phaseOffset) % ANIMATION_CONFIG.duration |
|||
const progress = phase / ANIMATION_CONFIG.duration // 0-1的进度
|
|||
|
|||
// 使用缓动函数使动画更平滑
|
|||
const easeProgress = 1 - Math.pow(1 - progress, 3) // ease-out cubic
|
|||
|
|||
// 计算半径和透明度
|
|||
const radius = |
|||
ANIMATION_CONFIG.minRadius + |
|||
easeProgress * (ANIMATION_CONFIG.maxRadius - ANIMATION_CONFIG.minRadius) |
|||
const opacity = Math.max(0, 1 - easeProgress) // 1到0的透明度
|
|||
|
|||
if (opacity > ANIMATION_CONFIG.minOpacity) { |
|||
// 计算颜色透明度
|
|||
const alpha = Math.floor(opacity * 255) |
|||
.toString(16) |
|||
.padStart(2, '0') |
|||
const strokeColor = color + alpha |
|||
|
|||
styles.push( |
|||
new Style({ |
|||
image: new Circle({ |
|||
radius: radius, |
|||
fill: new Fill({ |
|||
color: 'transparent' |
|||
}), |
|||
stroke: new Stroke({ |
|||
color: strokeColor, |
|||
width: Math.max(1, 3 - i * 0.4) // 动态调整线宽
|
|||
}) |
|||
}) |
|||
}) |
|||
) |
|||
} |
|||
} |
|||
|
|||
return styles |
|||
} |
|||
}) |
|||
|
|||
return this.rippleLayer |
|||
} |
|||
|
|||
/** |
|||
* 启动波纹动画 |
|||
*/ |
|||
startAnimation(): void { |
|||
let lastUpdateTime = 0 |
|||
const frameInterval = 1000 / ANIMATION_CONFIG.targetFPS // 帧间隔
|
|||
|
|||
const animateRipples = (currentTime: number) => { |
|||
if (this.rippleLayer && currentTime - lastUpdateTime >= frameInterval) { |
|||
this.rippleLayer.getSource()?.changed() |
|||
lastUpdateTime = currentTime |
|||
} |
|||
this.animationTimer = requestAnimationFrame(animateRipples) |
|||
} |
|||
animateRipples(0) |
|||
} |
|||
|
|||
/** |
|||
* 停止波纹动画 |
|||
*/ |
|||
stopAnimation(): void { |
|||
if (this.animationTimer) { |
|||
cancelAnimationFrame(this.animationTimer) |
|||
this.animationTimer = null |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 更新波纹图层 |
|||
*/ |
|||
updateRipples(): void { |
|||
if (this.rippleLayer) { |
|||
this.rippleLayer.getSource()?.changed() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取波纹图层 |
|||
*/ |
|||
getRippleLayer(): VectorLayer<VectorSource> | null { |
|||
return this.rippleLayer |
|||
} |
|||
|
|||
/** |
|||
* 销毁动画服务 |
|||
*/ |
|||
destroy(): void { |
|||
this.stopAnimation() |
|||
this.rippleLayer = null |
|||
this.map = null |
|||
} |
|||
} |
@ -0,0 +1,264 @@ |
|||
/** |
|||
* 围栏绘制服务类 |
|||
*/ |
|||
import { Vector as VectorLayer } from 'ol/layer' |
|||
import { Vector as VectorSource } from 'ol/source' |
|||
import { Draw, Modify, Snap } from 'ol/interaction' |
|||
import { Style, Stroke, Fill, Circle } from 'ol/style' |
|||
import { Polygon } from 'ol/geom' |
|||
import { Feature } from 'ol' |
|||
import { toLonLat, fromLonLat } from 'ol/proj' |
|||
import type { FenceData } from '../types/map.types' |
|||
|
|||
export class FenceDrawService { |
|||
private map: any = null |
|||
private drawLayer: VectorLayer<VectorSource> | null = null |
|||
private drawInteraction: Draw | null = null |
|||
private modifyInteraction: Modify | null = null |
|||
private snapInteraction: Snap | null = null |
|||
private isDrawing: boolean = false |
|||
|
|||
// 绘制完成回调
|
|||
private onDrawComplete: ((coordinates: [number, number][]) => void) | null = null |
|||
|
|||
/** |
|||
* 初始化绘制服务 |
|||
*/ |
|||
init(map: any): void { |
|||
this.map = map |
|||
this.createDrawLayer() |
|||
} |
|||
|
|||
/** |
|||
* 创建绘制图层 |
|||
*/ |
|||
private createDrawLayer(): void { |
|||
const source = new VectorSource() |
|||
|
|||
this.drawLayer = new VectorLayer({ |
|||
source: source, |
|||
style: new Style({ |
|||
stroke: new Stroke({ |
|||
color: '#409EFF', |
|||
width: 3, |
|||
lineDash: [5, 5] |
|||
}), |
|||
fill: new Fill({ |
|||
color: 'rgba(64, 158, 255, 0.1)' |
|||
}), |
|||
image: new Circle({ |
|||
radius: 6, |
|||
fill: new Fill({ |
|||
color: '#409EFF' |
|||
}), |
|||
stroke: new Stroke({ |
|||
color: '#fff', |
|||
width: 2 |
|||
}) |
|||
}) |
|||
}), |
|||
zIndex: 10 // 确保绘制图层在最上层
|
|||
}) |
|||
|
|||
this.map.addLayer(this.drawLayer) |
|||
} |
|||
|
|||
/** |
|||
* 开始绘制围栏 |
|||
*/ |
|||
startDrawing(onComplete: (coordinates: [number, number][]) => void): void { |
|||
if (this.isDrawing) { |
|||
this.stopDrawing() |
|||
} |
|||
|
|||
this.onDrawComplete = onComplete |
|||
this.isDrawing = true |
|||
|
|||
// 创建绘制交互
|
|||
this.drawInteraction = new Draw({ |
|||
source: this.drawLayer!.getSource()!, |
|||
type: 'Polygon', |
|||
style: new Style({ |
|||
stroke: new Stroke({ |
|||
color: '#409EFF', |
|||
width: 2, |
|||
lineDash: [5, 5] |
|||
}), |
|||
fill: new Fill({ |
|||
color: 'rgba(64, 158, 255, 0.1)' |
|||
}), |
|||
image: new Circle({ |
|||
radius: 6, |
|||
fill: new Fill({ |
|||
color: '#409EFF' |
|||
}), |
|||
stroke: new Stroke({ |
|||
color: '#fff', |
|||
width: 2 |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
// 监听绘制完成事件
|
|||
this.drawInteraction.on('drawend', (event) => { |
|||
const feature = event.feature |
|||
const geometry = feature.getGeometry() as Polygon |
|||
const coordinates = geometry.getCoordinates()[0] |
|||
|
|||
// 转换为经纬度坐标
|
|||
const lonLatCoordinates = coordinates.map((coord) => toLonLat(coord)) as [number, number][] |
|||
|
|||
// 移除最后一个重复的点
|
|||
if (lonLatCoordinates.length > 1) { |
|||
lonLatCoordinates.pop() |
|||
} |
|||
|
|||
// 调用完成回调
|
|||
if (this.onDrawComplete) { |
|||
this.onDrawComplete(lonLatCoordinates) |
|||
} |
|||
|
|||
// 立即清除绘制的特征,避免在正式围栏图层中重复显示
|
|||
setTimeout(() => { |
|||
if (this.drawLayer) { |
|||
const source = this.drawLayer.getSource() |
|||
if (source) { |
|||
source.clear() |
|||
} |
|||
} |
|||
}, 100) |
|||
|
|||
// 停止绘制
|
|||
this.stopDrawing() |
|||
}) |
|||
|
|||
// 创建修改交互
|
|||
this.modifyInteraction = new Modify({ |
|||
source: this.drawLayer!.getSource()! |
|||
}) |
|||
|
|||
// 创建捕捉交互
|
|||
this.snapInteraction = new Snap({ |
|||
source: this.drawLayer!.getSource()! |
|||
}) |
|||
|
|||
// 添加交互到地图
|
|||
this.map.addInteraction(this.drawInteraction) |
|||
this.map.addInteraction(this.modifyInteraction) |
|||
this.map.addInteraction(this.snapInteraction) |
|||
|
|||
// 改变鼠标样式
|
|||
this.map.getViewport().style.cursor = 'crosshair' |
|||
} |
|||
|
|||
/** |
|||
* 停止绘制 |
|||
*/ |
|||
stopDrawing(): void { |
|||
if (!this.isDrawing) return |
|||
|
|||
this.isDrawing = false |
|||
|
|||
// 移除交互
|
|||
if (this.drawInteraction) { |
|||
this.map.removeInteraction(this.drawInteraction) |
|||
this.drawInteraction = null |
|||
} |
|||
if (this.modifyInteraction) { |
|||
this.map.removeInteraction(this.modifyInteraction) |
|||
this.modifyInteraction = null |
|||
} |
|||
if (this.snapInteraction) { |
|||
this.map.removeInteraction(this.snapInteraction) |
|||
this.snapInteraction = null |
|||
} |
|||
|
|||
// 恢复鼠标样式
|
|||
this.map.getViewport().style.cursor = '' |
|||
|
|||
// 清空绘制图层
|
|||
if (this.drawLayer) { |
|||
const source = this.drawLayer.getSource() |
|||
if (source) { |
|||
source.clear() |
|||
} |
|||
} |
|||
|
|||
this.onDrawComplete = null |
|||
} |
|||
|
|||
/** |
|||
* 取消绘制 |
|||
*/ |
|||
cancelDrawing(): void { |
|||
this.stopDrawing() |
|||
} |
|||
|
|||
/** |
|||
* 检查是否正在绘制 |
|||
*/ |
|||
isCurrentlyDrawing(): boolean { |
|||
return this.isDrawing |
|||
} |
|||
|
|||
/** |
|||
* 清除绘制图层 |
|||
*/ |
|||
clearDrawLayer(): void { |
|||
if (this.drawLayer) { |
|||
const source = this.drawLayer.getSource() |
|||
if (source) { |
|||
source.clear() |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 显示编辑围栏 |
|||
*/ |
|||
showEditFence(fence: FenceData): void { |
|||
this.clearDrawLayer() |
|||
|
|||
if (this.drawLayer && fence.fenceRange.length > 0) { |
|||
// 将围栏坐标转换为地图坐标并创建多边形
|
|||
const coordinates = fence.fenceRange.map((coord) => [coord[0], coord[1]]) |
|||
|
|||
// 确保多边形闭合
|
|||
if (coordinates.length > 0) { |
|||
const lastCoord = coordinates[coordinates.length - 1] |
|||
const firstCoord = coordinates[0] |
|||
if (lastCoord[0] !== firstCoord[0] || lastCoord[1] !== firstCoord[1]) { |
|||
coordinates.push(firstCoord) |
|||
} |
|||
} |
|||
|
|||
const polygon = new Polygon([coordinates]) |
|||
polygon.transform('EPSG:4326', 'EPSG:3857') |
|||
|
|||
const feature = this.drawLayer.getSource()?.getFeatures()[0] |
|||
if (feature) { |
|||
feature.setGeometry(polygon) |
|||
} else { |
|||
const newFeature = new Feature({ |
|||
geometry: polygon |
|||
}) |
|||
this.drawLayer.getSource()?.addFeature(newFeature) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 销毁服务 |
|||
*/ |
|||
destroy(): void { |
|||
this.stopDrawing() |
|||
|
|||
if (this.drawLayer && this.map) { |
|||
this.map.removeLayer(this.drawLayer) |
|||
this.drawLayer = null |
|||
} |
|||
|
|||
this.map = null |
|||
} |
|||
} |
@ -0,0 +1,328 @@ |
|||
/** |
|||
* 围栏服务类 |
|||
*/ |
|||
import { Vector as VectorLayer } from 'ol/layer' |
|||
import { Vector as VectorSource } from 'ol/source' |
|||
import { Feature } from 'ol' |
|||
import { Polygon, Point } from 'ol/geom' |
|||
import { Style, Stroke, Fill, Circle, Text } from 'ol/style' |
|||
import { fromLonLat } from 'ol/proj' |
|||
import type { FenceData, MarkerData } from '../types/map.types' |
|||
|
|||
export class FenceService { |
|||
private fenceLayer: VectorLayer<VectorSource> | null = null |
|||
private fenceData: FenceData[] = [] |
|||
private map: any = null |
|||
private isVisible: boolean = true |
|||
|
|||
/** |
|||
* 创建围栏图层 |
|||
*/ |
|||
createFenceLayer(fences: FenceData[], map: any): VectorLayer<VectorSource> { |
|||
this.map = map |
|||
this.fenceData = fences |
|||
const source = new VectorSource() |
|||
|
|||
fences.forEach((fence) => { |
|||
// 创建围栏多边形特征
|
|||
const coordinates = fence.fenceRange.map((coord) => fromLonLat(coord)) |
|||
|
|||
// 确保围栏是闭合的
|
|||
if (coordinates.length > 0) { |
|||
const lastCoord = coordinates[coordinates.length - 1] |
|||
const firstCoord = coordinates[0] |
|||
if (lastCoord[0] !== firstCoord[0] || lastCoord[1] !== firstCoord[1]) { |
|||
coordinates.push(firstCoord) |
|||
} |
|||
} |
|||
|
|||
const feature = new Feature({ |
|||
geometry: new Polygon([coordinates]), |
|||
fenceData: fence |
|||
}) |
|||
|
|||
// 设置围栏样式
|
|||
feature.setStyle(this.createFenceStyle(fence)) |
|||
feature.set('type', 'fence') |
|||
feature.set('fenceId', fence.id) |
|||
source.addFeature(feature) |
|||
|
|||
// 创建围栏中心点标签
|
|||
const centerFeature = this.createFenceCenterLabel(fence, coordinates) |
|||
if (centerFeature) { |
|||
source.addFeature(centerFeature) |
|||
} |
|||
}) |
|||
|
|||
this.fenceLayer = new VectorLayer({ |
|||
source: source, |
|||
zIndex: 1 // 确保围栏在标记点下方
|
|||
}) |
|||
|
|||
return this.fenceLayer |
|||
} |
|||
|
|||
/** |
|||
* 创建围栏样式 |
|||
*/ |
|||
private createFenceStyle(fence: FenceData): Style { |
|||
let strokeColor = '#1890ff' |
|||
let fillColor = 'rgba(24, 144, 255, 0.1)' |
|||
let strokeWidth = 2 |
|||
|
|||
// 根据围栏状态设置样式
|
|||
switch (fence.status) { |
|||
case 0: |
|||
strokeColor = '#67c23a' |
|||
fillColor = 'rgba(103, 194, 58, 0.1)' |
|||
break |
|||
case 1: |
|||
strokeColor = '#e6a23c' |
|||
fillColor = 'rgba(230, 162, 60, 0.15)' |
|||
strokeWidth = 3 |
|||
break |
|||
case 2: |
|||
strokeColor = '#f56c6c' |
|||
fillColor = 'rgba(245, 108, 108, 0.2)' |
|||
strokeWidth = 4 |
|||
break |
|||
} |
|||
|
|||
// 根据围栏类型调整样式
|
|||
const lineDash = fence.type === 1 ? [10, 5] : undefined |
|||
|
|||
return new Style({ |
|||
stroke: new Stroke({ |
|||
color: strokeColor, |
|||
width: strokeWidth, |
|||
lineDash: lineDash |
|||
}), |
|||
fill: new Fill({ |
|||
color: fillColor |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 创建围栏中心点标签 |
|||
*/ |
|||
private createFenceCenterLabel(fence: FenceData, coordinates: number[][]): Feature | null { |
|||
if (coordinates.length === 0) return null |
|||
|
|||
// 计算围栏中心点
|
|||
const centerX = coordinates.reduce((sum, coord) => sum + coord[0], 0) / coordinates.length |
|||
const centerY = coordinates.reduce((sum, coord) => sum + coord[1], 0) / coordinates.length |
|||
|
|||
const labelFeature = new Feature({ |
|||
geometry: new Point([centerX, centerY]), |
|||
fenceData: fence |
|||
}) |
|||
|
|||
// 设置标签样式
|
|||
labelFeature.setStyle( |
|||
new Style({ |
|||
text: new Text({ |
|||
text: fence.name, |
|||
font: '12px Arial', |
|||
fill: new Fill({ |
|||
color: '#333' |
|||
}), |
|||
stroke: new Stroke({ |
|||
color: '#fff', |
|||
width: 2 |
|||
}), |
|||
backgroundFill: new Fill({ |
|||
color: 'rgba(255, 255, 255, 0.8)' |
|||
}), |
|||
backgroundStroke: new Stroke({ |
|||
color: '#ccc', |
|||
width: 1 |
|||
}), |
|||
padding: [2, 4, 2, 4] |
|||
}) |
|||
}) |
|||
) |
|||
|
|||
labelFeature.set('type', 'fence-label') |
|||
labelFeature.set('fenceId', fence.id) |
|||
|
|||
return labelFeature |
|||
} |
|||
|
|||
/** |
|||
* 获取围栏图层 |
|||
*/ |
|||
getFenceLayer(): VectorLayer<VectorSource> | null { |
|||
return this.fenceLayer |
|||
} |
|||
|
|||
/** |
|||
* 显示围栏 |
|||
*/ |
|||
showFences(): void { |
|||
if (this.fenceLayer) { |
|||
this.fenceLayer.setVisible(true) |
|||
this.isVisible = true |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 隐藏围栏 |
|||
*/ |
|||
hideFences(): void { |
|||
if (this.fenceLayer) { |
|||
this.fenceLayer.setVisible(false) |
|||
this.isVisible = false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 切换围栏显示状态 |
|||
*/ |
|||
toggleFences(): boolean { |
|||
if (this.isVisible) { |
|||
this.hideFences() |
|||
} else { |
|||
this.showFences() |
|||
} |
|||
return this.isVisible |
|||
} |
|||
|
|||
/** |
|||
* 设置围栏数据 |
|||
*/ |
|||
setFenceData(fences: FenceData[]): void { |
|||
this.fenceData = fences |
|||
if (this.fenceLayer && this.map) { |
|||
// 重新创建围栏图层
|
|||
const source = this.fenceLayer.getSource() |
|||
if (source) { |
|||
source.clear() |
|||
|
|||
fences.forEach((fence) => { |
|||
const coordinates = fence.fenceRange.map((coord) => fromLonLat(coord)) |
|||
|
|||
if (coordinates.length > 0) { |
|||
const lastCoord = coordinates[coordinates.length - 1] |
|||
const firstCoord = coordinates[0] |
|||
if (lastCoord[0] !== firstCoord[0] || lastCoord[1] !== firstCoord[1]) { |
|||
coordinates.push(firstCoord) |
|||
} |
|||
} |
|||
|
|||
const feature = new Feature({ |
|||
geometry: new Polygon([coordinates]), |
|||
fenceData: fence |
|||
}) |
|||
|
|||
feature.setStyle(this.createFenceStyle(fence)) |
|||
feature.set('type', 'fence') |
|||
feature.set('fenceId', fence.id) |
|||
source.addFeature(feature) |
|||
|
|||
const centerFeature = this.createFenceCenterLabel(fence, coordinates) |
|||
if (centerFeature) { |
|||
source.addFeature(centerFeature) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据ID获取围栏数据 |
|||
*/ |
|||
getFenceById(id: string): FenceData | undefined { |
|||
return this.fenceData.find((fence) => fence.id === id) |
|||
} |
|||
|
|||
/** |
|||
* 检查点是否在围栏内 |
|||
*/ |
|||
isPointInFence(point: [number, number], fenceId?: string): boolean { |
|||
const fences = fenceId ? this.fenceData.filter((fence) => fence.id === fenceId) : this.fenceData |
|||
|
|||
for (const fence of fences) { |
|||
if (this.pointInPolygon(point, fence.fenceRange)) { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 点在多边形内判断算法(射线法) |
|||
*/ |
|||
private pointInPolygon(point: [number, number], polygon: [number, number][]): boolean { |
|||
const [x, y] = point |
|||
let inside = false |
|||
|
|||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { |
|||
const [xi, yi] = polygon[i] |
|||
const [xj, yj] = polygon[j] |
|||
|
|||
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) { |
|||
inside = !inside |
|||
} |
|||
} |
|||
|
|||
return inside |
|||
} |
|||
|
|||
/** |
|||
* 获取标记点的围栏状态 |
|||
*/ |
|||
getMarkerFenceStatus(marker: MarkerData): { isInFence: boolean; fenceIds: string[] } { |
|||
const fenceIds: string[] = [] |
|||
|
|||
for (const fence of this.fenceData) { |
|||
if (this.pointInPolygon(marker.coordinates, fence.fenceRange)) { |
|||
fenceIds.push(fence.id) |
|||
} |
|||
} |
|||
|
|||
return { |
|||
isInFence: fenceIds.length > 0, |
|||
fenceIds |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 更新围栏状态 |
|||
*/ |
|||
updateFenceStatus(fenceId: string, status: number): void { |
|||
const fence = this.fenceData.find((f) => f.id === fenceId) |
|||
if (fence) { |
|||
fence.status = status |
|||
|
|||
// 更新图层中对应的特征样式
|
|||
if (this.fenceLayer) { |
|||
const source = this.fenceLayer.getSource() |
|||
if (source) { |
|||
const features = source.getFeatures() |
|||
const fenceFeature = features.find( |
|||
(feature) => feature.get('type') === 'fence' && feature.get('fenceId') === fenceId |
|||
) |
|||
if (fenceFeature) { |
|||
fenceFeature.setStyle(this.createFenceStyle(fence)) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 销毁服务 |
|||
*/ |
|||
destroy(): void { |
|||
if (this.fenceLayer) { |
|||
const source = this.fenceLayer.getSource() |
|||
if (source) { |
|||
source.clear() |
|||
} |
|||
} |
|||
this.fenceLayer = null |
|||
this.fenceData = [] |
|||
this.map = null |
|||
} |
|||
} |
@ -0,0 +1,114 @@ |
|||
/** |
|||
* 地图服务类 |
|||
*/ |
|||
import { Map, View } from 'ol' |
|||
import { Tile as TileLayer } from 'ol/layer' |
|||
import { OSM, XYZ } from 'ol/source' |
|||
import { fromLonLat } from 'ol/proj' |
|||
import Overlay from 'ol/Overlay' |
|||
import type { MapProps, MapInstance } from '../types/map.types' |
|||
|
|||
export class MapService { |
|||
private map: Map | null = null |
|||
private tileLayer: TileLayer<XYZ | OSM> | null = null |
|||
private popupOverlay: Overlay | null = null |
|||
|
|||
/** |
|||
* 创建瓦片图层 |
|||
*/ |
|||
private createTileLayer(props: MapProps): TileLayer<XYZ | OSM> { |
|||
const source = new XYZ({ |
|||
url: props.tileUrl!, |
|||
maxZoom: props.maxZoom, |
|||
minZoom: props.minZoom |
|||
}) |
|||
return new TileLayer({ |
|||
source: source |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 创建弹窗 |
|||
*/ |
|||
private createPopup(): Overlay { |
|||
const popupElement = document.createElement('div') |
|||
popupElement.className = 'marker-popup' |
|||
popupElement.style.cssText = ` |
|||
background: white; |
|||
border: 1px solid #ccc; |
|||
border-radius: 6px; |
|||
padding: 12px 16px; |
|||
box-shadow: 0 4px 12px rgba(0,0,0,0.15); |
|||
font-size: 12px; |
|||
max-width: 280px; |
|||
min-width: 200px; |
|||
pointer-events: none; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|||
` |
|||
|
|||
this.popupOverlay = new Overlay({ |
|||
element: popupElement, |
|||
positioning: 'bottom-center', |
|||
stopEvent: false, |
|||
offset: [0, -10] |
|||
}) |
|||
|
|||
return this.popupOverlay |
|||
} |
|||
|
|||
/** |
|||
* 初始化地图 |
|||
*/ |
|||
initMap(container: HTMLElement, props: MapProps): MapInstance { |
|||
this.tileLayer = this.createTileLayer(props) |
|||
const popup = this.createPopup() |
|||
|
|||
const center = fromLonLat(props.center!) |
|||
|
|||
this.map = new Map({ |
|||
target: container, |
|||
layers: [this.tileLayer], |
|||
overlays: [popup], |
|||
view: new View({ |
|||
center: center, |
|||
zoom: props.zoom, |
|||
maxZoom: props.maxZoom, |
|||
minZoom: props.minZoom |
|||
}) |
|||
}) |
|||
|
|||
return { |
|||
map: this.map, |
|||
tileLayer: this.tileLayer, |
|||
markerLayer: null, |
|||
rippleLayer: null, |
|||
popupOverlay: this.popupOverlay |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取地图实例 |
|||
*/ |
|||
getMap(): Map | null { |
|||
return this.map |
|||
} |
|||
|
|||
/** |
|||
* 获取弹窗实例 |
|||
*/ |
|||
getPopupOverlay(): Overlay | null { |
|||
return this.popupOverlay |
|||
} |
|||
|
|||
/** |
|||
* 销毁地图 |
|||
*/ |
|||
destroy(): void { |
|||
if (this.map) { |
|||
this.map.setTarget(undefined) |
|||
this.map = null |
|||
this.tileLayer = null |
|||
this.popupOverlay = null |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,196 @@ |
|||
/** |
|||
* 标记服务类 |
|||
*/ |
|||
import { Vector as VectorLayer } from 'ol/layer' |
|||
import { Vector as VectorSource, Cluster } from 'ol/source' |
|||
import { Feature } from 'ol' |
|||
import { Point } from 'ol/geom' |
|||
import { Style } from 'ol/style' |
|||
import { fromLonLat } from 'ol/proj' |
|||
import type { MarkerData, MapProps } from '../types/map.types' |
|||
import { createMarkerStyle, getClusterMarkerData } from '../utils/map.utils' |
|||
|
|||
export class MarkerService { |
|||
private markerLayer: VectorLayer<VectorSource | Cluster> | null = null |
|||
private currentProps: MapProps | null = null |
|||
private map: any = null |
|||
|
|||
/** |
|||
* 设置地图实例 |
|||
*/ |
|||
setMap(map: any): void { |
|||
this.map = map |
|||
} |
|||
|
|||
/** |
|||
* 创建标记图层 |
|||
*/ |
|||
createMarkerLayer(props: MapProps, map?: any): VectorLayer<VectorSource | Cluster> { |
|||
// 保存地图实例
|
|||
if (map) { |
|||
this.map = map |
|||
} |
|||
|
|||
// 保存当前props
|
|||
this.currentProps = { ...props } |
|||
|
|||
const source = new VectorSource() |
|||
// 添加标记
|
|||
const markers = props.markers || [] |
|||
markers.forEach((marker) => { |
|||
const feature = new Feature({ |
|||
geometry: new Point(fromLonLat(marker.coordinates)), |
|||
markerData: marker |
|||
}) |
|||
feature.setStyle(createMarkerStyle(marker)) |
|||
source.addFeature(feature) |
|||
}) |
|||
|
|||
// 检查是否应该强制使用单个marker模式
|
|||
const shouldForceSingleMark = () => { |
|||
if (!props.forceSingleMark || !this.map) return false |
|||
const currentZoom = this.map.getView().getZoom() |
|||
return currentZoom >= props.forceSingleMark |
|||
} |
|||
|
|||
// 如果启用聚合且不强制使用单个marker模式
|
|||
if (props.enableCluster && !shouldForceSingleMark()) { |
|||
const clusterSource = new Cluster({ |
|||
source: source, |
|||
distance: Math.max(props.clusterDistance || 40, 10) // 确保最小距离为10像素
|
|||
}) |
|||
|
|||
this.markerLayer = new VectorLayer({ |
|||
source: clusterSource, |
|||
style: (feature) => { |
|||
const features = feature.get('features') |
|||
|
|||
// 确保features存在且不为空
|
|||
if (!features || features.length === 0) { |
|||
return new Style() // 返回空样式,隐藏无效的feature
|
|||
} |
|||
|
|||
if (features.length === 1) { |
|||
// 单个marker
|
|||
const markerData = features[0].get('markerData') |
|||
return markerData ? createMarkerStyle(markerData) : new Style() |
|||
} else { |
|||
// 聚合marker
|
|||
const highestStatus = getClusterMarkerData(features) |
|||
return createMarkerStyle(highestStatus, true, features.length) |
|||
} |
|||
} |
|||
}) |
|||
} else { |
|||
this.markerLayer = new VectorLayer({ |
|||
source: source |
|||
}) |
|||
} |
|||
|
|||
return this.markerLayer |
|||
} |
|||
|
|||
/** |
|||
* 获取标记图层 |
|||
*/ |
|||
getMarkerLayer(): VectorLayer<VectorSource | Cluster> | null { |
|||
return this.markerLayer |
|||
} |
|||
|
|||
/** |
|||
* 更新标记数据 |
|||
*/ |
|||
updateMarkers(markers: MarkerData[]): void { |
|||
if (!this.currentProps) return |
|||
|
|||
// 更新props中的markers
|
|||
this.currentProps.markers = markers |
|||
|
|||
// 完全重新创建markerLayer
|
|||
const newLayer = this.createMarkerLayerFromProps(this.currentProps) |
|||
|
|||
// 如果有旧的layer,将其从地图中移除
|
|||
if (this.markerLayer) { |
|||
// 这里需要外部调用来移除旧layer并添加新layer
|
|||
// 我们只负责创建新的layer
|
|||
} |
|||
|
|||
this.markerLayer = newLayer |
|||
} |
|||
|
|||
/** |
|||
* 从props创建markerLayer(内部方法) |
|||
*/ |
|||
private createMarkerLayerFromProps(props: MapProps): VectorLayer<VectorSource | Cluster> { |
|||
const source = new VectorSource() |
|||
// 添加标记
|
|||
const markers = props.markers || [] |
|||
markers.forEach((marker) => { |
|||
const feature = new Feature({ |
|||
geometry: new Point(fromLonLat(marker.coordinates)), |
|||
markerData: marker |
|||
}) |
|||
feature.setStyle(createMarkerStyle(marker)) |
|||
source.addFeature(feature) |
|||
}) |
|||
|
|||
// 检查是否应该强制使用单个marker模式
|
|||
const shouldForceSingleMark = () => { |
|||
if (!props.forceSingleMark || !this.map) return false |
|||
const currentZoom = this.map.getView().getZoom() |
|||
return currentZoom >= props.forceSingleMark |
|||
} |
|||
|
|||
// 如果启用聚合且不强制使用单个marker模式
|
|||
if (props.enableCluster && !shouldForceSingleMark()) { |
|||
const clusterSource = new Cluster({ |
|||
source: source, |
|||
distance: Math.max(props.clusterDistance || 40, 10) |
|||
}) |
|||
|
|||
return new VectorLayer({ |
|||
source: clusterSource, |
|||
style: (feature) => { |
|||
const features = feature.get('features') |
|||
|
|||
// 确保features存在且不为空
|
|||
if (!features || features.length === 0) { |
|||
return new Style() // 返回空样式,隐藏无效的feature
|
|||
} |
|||
|
|||
if (features.length === 1) { |
|||
// 单个marker
|
|||
const markerData = features[0].get('markerData') |
|||
return markerData ? createMarkerStyle(markerData) : new Style() |
|||
} else { |
|||
// 聚合marker
|
|||
const highestStatus = getClusterMarkerData(features) |
|||
return createMarkerStyle(highestStatus, true, features.length) |
|||
} |
|||
} |
|||
}) |
|||
} else { |
|||
return new VectorLayer({ |
|||
source: source |
|||
}) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 刷新标记样式(用于聚合更新) |
|||
*/ |
|||
refreshStyles(): void { |
|||
if (this.markerLayer) { |
|||
this.markerLayer.changed() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 销毁标记图层 |
|||
*/ |
|||
destroy(): void { |
|||
this.markerLayer = null |
|||
this.currentProps = null |
|||
this.map = null |
|||
} |
|||
} |
@ -0,0 +1,86 @@ |
|||
/** |
|||
* 弹窗服务类 |
|||
*/ |
|||
import type { MarkerData, DetectorInfo } from '../types/map.types' |
|||
import { |
|||
getHighestPriorityStatus, |
|||
getStatusLabel, |
|||
getStatusColor, |
|||
createClusterPopupHTML, |
|||
sortDetectorsByPriority |
|||
} from '../utils/map.utils' |
|||
|
|||
export class PopupService { |
|||
/** |
|||
* 处理聚合标记弹窗 |
|||
*/ |
|||
handleClusterPopup(features: any[]): string { |
|||
// 收集所有探测器信息
|
|||
const detectorList: DetectorInfo[] = features.map((f) => { |
|||
const markerData = f.get('markerData') as MarkerData |
|||
const status = getHighestPriorityStatus(markerData) |
|||
return { |
|||
name: markerData.name, |
|||
status, |
|||
statusLabel: getStatusLabel(status), |
|||
statusColor: getStatusColor(status) |
|||
} |
|||
}) |
|||
|
|||
// 按优先级排序
|
|||
const sortedDetectorList = sortDetectorsByPriority(detectorList) |
|||
|
|||
// 生成弹窗HTML
|
|||
return createClusterPopupHTML(sortedDetectorList) |
|||
} |
|||
|
|||
/** |
|||
* 处理单个标记弹窗 |
|||
*/ |
|||
handleSingleMarkerPopup(markerData: MarkerData): string { |
|||
const status = getHighestPriorityStatus(markerData) |
|||
return ` |
|||
<div style="font-weight: bold; margin-bottom: 4px;">${markerData.name}</div> |
|||
<div style="color: ${getStatusColor(status)};"> |
|||
状态: ${getStatusLabel(status)} |
|||
</div> |
|||
<div style="margin-top: 4px; font-size: 10px; color: #666;"> |
|||
坐标: ${markerData.coordinates[0].toFixed(6)}, ${markerData.coordinates[1].toFixed(6)} |
|||
</div> |
|||
` |
|||
} |
|||
|
|||
/** |
|||
* 处理默认弹窗 |
|||
*/ |
|||
handleDefaultPopup(): string { |
|||
return ` |
|||
<div style="font-weight: bold;">标记</div> |
|||
<div style="font-size: 10px; color: #666;">未知标记</div> |
|||
` |
|||
} |
|||
|
|||
/** |
|||
* 根据特征类型处理弹窗内容 |
|||
*/ |
|||
handlePopupContent(feature: any): string { |
|||
const markerData = feature.get('markerData') |
|||
const features = feature.get('features') |
|||
|
|||
// 处理聚合标记
|
|||
if (features && features.length > 1) { |
|||
return this.handleClusterPopup(features) |
|||
} else if (features && features.length === 1) { |
|||
// 处理聚合中的单个标记
|
|||
const singleMarkerData = features[0].get('markerData') as MarkerData |
|||
if (singleMarkerData) { |
|||
return this.handleSingleMarkerPopup(singleMarkerData) |
|||
} |
|||
} else if (markerData) { |
|||
// 处理非聚合的单个标记
|
|||
return this.handleSingleMarkerPopup(markerData as MarkerData) |
|||
} |
|||
|
|||
return this.handleDefaultPopup() |
|||
} |
|||
} |
@ -0,0 +1,502 @@ |
|||
/** |
|||
* 轨迹服务类 |
|||
*/ |
|||
import { Vector as VectorLayer } from 'ol/layer' |
|||
import { Vector as VectorSource } from 'ol/source' |
|||
import { Feature } from 'ol' |
|||
import { LineString, Point } from 'ol/geom' |
|||
import { Style, Stroke, Circle, Fill, Text, Icon } from 'ol/style' |
|||
import { fromLonLat } from 'ol/proj' |
|||
import type { |
|||
TrajectoryData, |
|||
TrajectoryPoint, |
|||
TrajectoryPlayState, |
|||
MarkerData |
|||
} from '../types/map.types' |
|||
import { createLocationIconSVG, getHighestPriorityStatus, getStatusColor } from '../utils/map.utils' |
|||
import dayjs from 'dayjs' |
|||
|
|||
export class TrajectoryService { |
|||
private trajectoryLayer: VectorLayer<VectorSource> | null = null |
|||
private trajectoryData: TrajectoryData[] = [] |
|||
private map: any = null |
|||
private animationTimer: number | null = null |
|||
|
|||
// 当前移动的 marker 图层
|
|||
private movingMarkerLayer: VectorLayer<VectorSource> | null = null |
|||
|
|||
// 按时间排序的所有轨迹点
|
|||
private sortedTrajectoryPoints: Array<TrajectoryPoint> = [] |
|||
|
|||
/** |
|||
* 创建轨迹图层 |
|||
*/ |
|||
createTrajectoryLayer(map: any): VectorLayer<VectorSource> { |
|||
this.map = map |
|||
const source = new VectorSource() |
|||
|
|||
this.trajectoryLayer = new VectorLayer({ |
|||
source: source, |
|||
style: (feature) => { |
|||
const featureType = feature.get('type') |
|||
|
|||
if (featureType === 'trajectory') { |
|||
// 轨迹线条样式
|
|||
return new Style({ |
|||
stroke: new Stroke({ |
|||
color: feature.get('color') || '#1890ff', |
|||
width: feature.get('width') || 3, |
|||
lineDash: [5, 5] // 虚线效果
|
|||
}) |
|||
}) |
|||
} else if (featureType === 'trajectory-point') { |
|||
// 轨迹点样式
|
|||
const isActive = feature.get('isActive') |
|||
const pointRadius = feature.get('pointRadius') || 4 |
|||
const activePointRadius = feature.get('activePointRadius') || 8 |
|||
const showTimeLabel = feature.get('showTimeLabel') !== false // 默认显示时间标签
|
|||
|
|||
// 根据轨迹点的data状态获取颜色
|
|||
const pointData = feature.get('pointData') |
|||
let color = feature.get('color') || '#1890ff' // 默认颜色
|
|||
|
|||
if (pointData) { |
|||
// 将点数据构造为MarkerData格式以使用现有的状态计算函数
|
|||
const markerData = { |
|||
id: -1, |
|||
coordinates: [0, 0] as [number, number], |
|||
name: '', |
|||
gasStatus: pointData.gasStatus || '0', |
|||
batteryStatus: pointData.batteryStatus || '0', |
|||
fenceStatus: pointData.fenceStatus || '0', |
|||
onlineStatus: pointData.onlineStatus || '1' |
|||
} |
|||
const status = getHighestPriorityStatus(markerData) |
|||
color = getStatusColor(status) |
|||
} |
|||
|
|||
return new Style({ |
|||
image: new Circle({ |
|||
radius: isActive ? activePointRadius : pointRadius, |
|||
fill: new Fill({ |
|||
color: isActive ? color : '#ffffff' |
|||
}), |
|||
stroke: new Stroke({ |
|||
color: color, |
|||
width: isActive ? 3 : 2 |
|||
}) |
|||
}), |
|||
text: |
|||
isActive && showTimeLabel |
|||
? new Text({ |
|||
text: feature.get('timeText') || '', |
|||
font: '12px Arial', |
|||
fill: new Fill({ |
|||
color: '#333' |
|||
}), |
|||
offsetY: -15, |
|||
backgroundFill: new Fill({ |
|||
color: 'rgba(255, 255, 255, 0.8)' |
|||
}), |
|||
padding: [2, 4, 2, 4] |
|||
}) |
|||
: undefined |
|||
}) |
|||
} |
|||
|
|||
return new Style() |
|||
} |
|||
}) |
|||
|
|||
// 创建移动中的 marker 图层
|
|||
this.movingMarkerLayer = new VectorLayer({ |
|||
source: new VectorSource(), |
|||
style: (feature) => { |
|||
const color = feature.get('color') || '#ff4757' |
|||
return new Style({ |
|||
image: new Icon({ |
|||
src: createLocationIconSVG(color, 32), // 使用位置图标,大小为32px
|
|||
scale: 1, |
|||
anchor: [0.5, 1], // 锚点设置在底部中心
|
|||
anchorXUnits: 'fraction', |
|||
anchorYUnits: 'fraction' |
|||
}), |
|||
text: new Text({ |
|||
text: feature.get('deviceName') || '', |
|||
font: 'bold 12px Arial', |
|||
fill: new Fill({ |
|||
color: '#ffffff' |
|||
}), |
|||
offsetY: -40, // 调整文字位置,避免与图标重叠
|
|||
backgroundFill: new Fill({ |
|||
color: 'rgba(0, 0, 0, 0.7)' |
|||
}), |
|||
padding: [2, 6, 2, 6] |
|||
}) |
|||
}) |
|||
} |
|||
}) |
|||
|
|||
map.addLayer(this.movingMarkerLayer) |
|||
return this.trajectoryLayer |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹数据并从 markers 中提取轨迹 |
|||
*/ |
|||
setTrajectoryData(markers: MarkerData[]): void { |
|||
// 从 markers 中提取轨迹数据
|
|||
this.trajectoryData = markers |
|||
.filter((marker) => marker.data && marker.data.length > 0) |
|||
.map((marker) => ({ |
|||
deviceId: marker.id, |
|||
name: marker.name, |
|||
points: marker.data.map((item: any) => ({ |
|||
coordinates: [item.lng, item.lat] as [number, number], |
|||
timestamp: dayjs(item.time, 'YYYY-MM-DD HH:mm:ss').valueOf(), |
|||
data: item |
|||
})), |
|||
color: '#1890ff', |
|||
width: 3, |
|||
pointRadius: 2, // 默认轨迹点半径
|
|||
activePointRadius: 2, // 默认激活状态轨迹点半径
|
|||
showTimeLabel: false // 默认显示时间标签
|
|||
})) |
|||
|
|||
// 创建按时间排序的所有轨迹点
|
|||
this.sortedTrajectoryPoints = [] |
|||
this.trajectoryData.forEach((trajectory) => { |
|||
trajectory.points.forEach((point, index) => { |
|||
this.sortedTrajectoryPoints.push({ |
|||
...point, |
|||
deviceId: trajectory.deviceId, |
|||
pointIndex: index |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
// 按时间戳排序
|
|||
this.sortedTrajectoryPoints.sort((a, b) => a.timestamp - b.timestamp) |
|||
|
|||
this.renderTrajectories() |
|||
} |
|||
|
|||
/** |
|||
* 根据时间范围过滤轨迹数据 |
|||
*/ |
|||
filterTrajectoryByTimeRange(startTime: number, endTime: number): void { |
|||
this.sortedTrajectoryPoints = this.sortedTrajectoryPoints.filter( |
|||
(point) => point.timestamp >= startTime && point.timestamp <= endTime |
|||
) |
|||
this.renderTrajectories() |
|||
} |
|||
|
|||
/** |
|||
* 根据当前播放时间更新显示 |
|||
*/ |
|||
updateByPlayState(playState: TrajectoryPlayState): void { |
|||
if (this.sortedTrajectoryPoints.length === 0) return |
|||
|
|||
// 为每个设备找到当前时间对应的轨迹点
|
|||
const currentPoints = this.findPointsForAllDevicesByTime(playState.currentTime) |
|||
|
|||
if (currentPoints.length > 0) { |
|||
// 找到对应点时,更新所有设备的移动marker和轨迹进度
|
|||
this.updateAllMovingMarkers(currentPoints) |
|||
this.updateTrajectoryProgress(playState.currentTime) |
|||
} else { |
|||
// 没有找到对应点时(可能时间在轨迹数据开始之前),清除移动marker并重置所有轨迹点状态
|
|||
this.clearMovingMarker() |
|||
this.updateTrajectoryProgress(playState.currentTime) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 为所有设备查找指定时间戳对应的轨迹点 |
|||
*/ |
|||
private findPointsForAllDevicesByTime(timestamp: number): Array<TrajectoryPoint> { |
|||
const result: Array<TrajectoryPoint> = [] |
|||
|
|||
// 为每个设备找到对应时间点的最近轨迹点
|
|||
this.trajectoryData.forEach((trajectory) => { |
|||
// 找到该设备在指定时间的最近轨迹点
|
|||
let closestPoint: TrajectoryPoint | null = null |
|||
let minDiff = Infinity |
|||
|
|||
trajectory.points.forEach((point) => { |
|||
if (point.timestamp <= timestamp) { |
|||
const diff = Math.abs(point.timestamp - timestamp) |
|||
if (diff < minDiff) { |
|||
minDiff = diff |
|||
closestPoint = { |
|||
...point, |
|||
deviceId: trajectory.deviceId |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
|
|||
if (closestPoint) { |
|||
result.push(closestPoint) |
|||
} |
|||
}) |
|||
|
|||
return result |
|||
} |
|||
|
|||
/** |
|||
* 更新所有设备的移动中的 marker |
|||
*/ |
|||
private updateAllMovingMarkers(points: Array<TrajectoryPoint>): void { |
|||
if (!this.movingMarkerLayer) return |
|||
|
|||
const source = this.movingMarkerLayer.getSource() |
|||
source?.clear() |
|||
|
|||
// 为每个设备创建移动marker
|
|||
points.forEach((point) => { |
|||
// 找到对应设备的信息
|
|||
const trajectory = this.trajectoryData.find((t) => t.deviceId === point.deviceId) |
|||
if (!trajectory) return |
|||
|
|||
// 根据轨迹点的data状态获取颜色
|
|||
let color = trajectory.color || '#ff4757' // 默认颜色
|
|||
|
|||
if (point.data) { |
|||
// 将点数据构造为MarkerData格式以使用现有的状态计算函数
|
|||
const markerData = { |
|||
id: -1, |
|||
coordinates: [0, 0] as [number, number], |
|||
name: '', |
|||
gasStatus: point.data.gasStatus || '0', |
|||
batteryStatus: point.data.batteryStatus || '0', |
|||
fenceStatus: point.data.fenceStatus || '0', |
|||
onlineStatus: point.data.onlineStatus || '1' |
|||
} |
|||
const status = getHighestPriorityStatus(markerData) |
|||
color = getStatusColor(status) |
|||
} |
|||
|
|||
const markerFeature = new Feature({ |
|||
geometry: new Point(fromLonLat(point.coordinates)), |
|||
type: 'moving-marker', |
|||
deviceId: point.deviceId, |
|||
deviceName: trajectory.name, |
|||
color: color, |
|||
timestamp: point.timestamp |
|||
}) |
|||
|
|||
source?.addFeature(markerFeature) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 清除移动中的 marker |
|||
*/ |
|||
private clearMovingMarker(): void { |
|||
if (!this.movingMarkerLayer) return |
|||
|
|||
const source = this.movingMarkerLayer.getSource() |
|||
source?.clear() |
|||
} |
|||
|
|||
/** |
|||
* 更新轨迹进度显示 |
|||
*/ |
|||
private updateTrajectoryProgress(currentTime: number): void { |
|||
if (!this.trajectoryLayer) return |
|||
|
|||
const source = this.trajectoryLayer.getSource() |
|||
if (!source) return |
|||
|
|||
// 获取所有轨迹点 features
|
|||
const features = source.getFeatures() |
|||
|
|||
// 强制更新所有轨迹点的激活状态
|
|||
features.forEach((feature) => { |
|||
if (feature.get('type') === 'trajectory-point') { |
|||
const pointTime = feature.get('timestamp') |
|||
const wasActive = feature.get('isActive') |
|||
const isActive = pointTime <= currentTime |
|||
|
|||
// 只有状态真正变化时才设置,以触发重新渲染
|
|||
if (wasActive !== isActive) { |
|||
feature.set('isActive', isActive) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
// 强制触发重新渲染
|
|||
source.changed() |
|||
|
|||
// 额外确保样式更新
|
|||
this.trajectoryLayer.changed() |
|||
} |
|||
|
|||
/** |
|||
* 渲染轨迹 |
|||
*/ |
|||
private renderTrajectories(): void { |
|||
if (!this.trajectoryLayer) return |
|||
|
|||
const source = this.trajectoryLayer.getSource() |
|||
source?.clear() |
|||
|
|||
this.trajectoryData.forEach((trajectory) => { |
|||
if (trajectory.points.length < 2) return |
|||
|
|||
// 创建轨迹线条
|
|||
const coordinates = trajectory.points.map((point) => fromLonLat(point.coordinates)) |
|||
const lineFeature = new Feature({ |
|||
geometry: new LineString(coordinates), |
|||
type: 'trajectory', |
|||
color: trajectory.color || '#1890ff', |
|||
width: trajectory.width || 3, |
|||
deviceId: trajectory.deviceId |
|||
}) |
|||
source?.addFeature(lineFeature) |
|||
|
|||
// 创建轨迹点
|
|||
trajectory.points.forEach((point, index) => { |
|||
const pointFeature = new Feature({ |
|||
geometry: new Point(fromLonLat(point.coordinates)), |
|||
type: 'trajectory-point', |
|||
color: trajectory.color || '#1890ff', |
|||
isActive: false, // 初始状态为非激活
|
|||
timeText: this.formatTime(point.timestamp), |
|||
pointIndex: index, |
|||
trajectoryId: trajectory.deviceId, |
|||
timestamp: point.timestamp, |
|||
pointRadius: trajectory.pointRadius || 2, // 传递轨迹点半径
|
|||
activePointRadius: trajectory.activePointRadius || 2, // 传递激活状态轨迹点半径
|
|||
showTimeLabel: trajectory.showTimeLabel !== false, // 传递时间标签显示控制
|
|||
pointData: point.data // 添加点数据以便样式函数使用
|
|||
}) |
|||
source?.addFeature(pointFeature) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置轨迹点大小 |
|||
*/ |
|||
setTrajectoryPointSize(deviceId: number, pointRadius: number, activePointRadius?: number): void { |
|||
const trajectory = this.trajectoryData.find((t) => t.deviceId === deviceId) |
|||
if (trajectory) { |
|||
trajectory.pointRadius = pointRadius |
|||
if (activePointRadius !== undefined) { |
|||
trajectory.activePointRadius = activePointRadius |
|||
} |
|||
this.renderTrajectories() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置所有轨迹点大小 |
|||
*/ |
|||
setAllTrajectoryPointSize(pointRadius: number, activePointRadius?: number): void { |
|||
this.trajectoryData.forEach((trajectory) => { |
|||
trajectory.pointRadius = pointRadius |
|||
if (activePointRadius !== undefined) { |
|||
trajectory.activePointRadius = activePointRadius |
|||
} |
|||
}) |
|||
this.renderTrajectories() |
|||
} |
|||
|
|||
/** |
|||
* 设置时间标签显示状态 |
|||
*/ |
|||
setTimeLabelsVisible(deviceId: number, visible: boolean): void { |
|||
const trajectory = this.trajectoryData.find((t) => t.deviceId === deviceId) |
|||
if (trajectory) { |
|||
trajectory.showTimeLabel = visible |
|||
this.renderTrajectories() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置所有时间标签显示状态 |
|||
*/ |
|||
setAllTimeLabelsVisible(visible: boolean): void { |
|||
this.trajectoryData.forEach((trajectory) => { |
|||
trajectory.showTimeLabel = visible |
|||
}) |
|||
this.renderTrajectories() |
|||
} |
|||
|
|||
/** |
|||
* 格式化时间 |
|||
*/ |
|||
private formatTime(timestamp: number): string { |
|||
return dayjs(timestamp).format('HH:mm:ss') |
|||
} |
|||
|
|||
/** |
|||
* 显示轨迹(当 showTrajectoryControls 为 true 时调用) |
|||
*/ |
|||
showTrajectories(): void { |
|||
if (this.trajectoryLayer) { |
|||
this.trajectoryLayer.setVisible(true) |
|||
} |
|||
if (this.movingMarkerLayer) { |
|||
this.movingMarkerLayer.setVisible(true) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 隐藏轨迹 |
|||
*/ |
|||
hideTrajectories(): void { |
|||
if (this.trajectoryLayer) { |
|||
this.trajectoryLayer.setVisible(false) |
|||
} |
|||
if (this.movingMarkerLayer) { |
|||
this.movingMarkerLayer.setVisible(false) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取轨迹数据 |
|||
*/ |
|||
getTrajectoryData(): TrajectoryData[] { |
|||
return [...this.trajectoryData] |
|||
} |
|||
|
|||
/** |
|||
* 获取轨迹图层 |
|||
*/ |
|||
getTrajectoryLayer(): VectorLayer<VectorSource> | null { |
|||
return this.trajectoryLayer |
|||
} |
|||
|
|||
/** |
|||
* 获取移动标记图层 |
|||
*/ |
|||
getMovingMarkerLayer(): VectorLayer<VectorSource> | null { |
|||
return this.movingMarkerLayer |
|||
} |
|||
|
|||
/** |
|||
* 获取地图实例(用于扩展功能) |
|||
*/ |
|||
getMap(): any { |
|||
return this.map |
|||
} |
|||
|
|||
/** |
|||
* 销毁轨迹服务 |
|||
*/ |
|||
destroy(): void { |
|||
if (this.animationTimer) { |
|||
clearTimeout(this.animationTimer) |
|||
this.animationTimer = null |
|||
} |
|||
|
|||
this.trajectoryLayer = null |
|||
this.movingMarkerLayer = null |
|||
this.map = null // 清理 map 引用
|
|||
this.trajectoryData = [] |
|||
this.sortedTrajectoryPoints = [] |
|||
} |
|||
} |
@ -0,0 +1,151 @@ |
|||
/** |
|||
* 地图组件相关类型定义 |
|||
*/ |
|||
import { HandDetector } from '@/api/gas/handdetector' |
|||
// 状态字典配置
|
|||
export interface StatusDictItem { |
|||
value: string |
|||
label: string |
|||
cssClass: string |
|||
} |
|||
|
|||
// 标记状态类型
|
|||
export type MarkerStatus = string |
|||
|
|||
// 标记数据接口
|
|||
export interface MarkerData extends HandDetector { |
|||
/** 坐标 [经度, 纬度] */ |
|||
coordinates: [number, number] |
|||
/** 气体状态 */ |
|||
gasStatus?: MarkerStatus |
|||
/** 电池状态 */ |
|||
batteryStatus?: MarkerStatus |
|||
/** 围栏状态 */ |
|||
fenceStatus?: MarkerStatus |
|||
/** 在线状态 */ |
|||
onlineStatus?: MarkerStatus |
|||
/** 标记标题 */ |
|||
name: string |
|||
/** 自定义数据 */ |
|||
data?: any |
|||
} |
|||
|
|||
// 地图组件 Props 接口
|
|||
export interface MapProps { |
|||
/** 自定义瓦片图地址模板,支持 {x}, {y}, {z} 占位符 */ |
|||
tileUrl?: string |
|||
/** 地图中心点坐标 [经度, 纬度] */ |
|||
center?: [number, number] |
|||
/** 初始缩放级别 */ |
|||
zoom?: number |
|||
/** 最大缩放级别 */ |
|||
maxZoom?: number |
|||
/** 最小缩放级别 */ |
|||
minZoom?: number |
|||
/** 标记数据列表 */ |
|||
markers?: MarkerData[] |
|||
/** 围栏数据列表 */ |
|||
fences?: FenceData[] |
|||
/** 是否启用聚合功能 */ |
|||
enableCluster?: boolean |
|||
/** 聚合距离(像素) */ |
|||
clusterDistance?: number |
|||
/** 强制单个marker显示的zoom级别阈值 */ |
|||
forceSingleMark?: number |
|||
/** 是否显示轨迹控制面板 */ |
|||
showTrajectories?: boolean |
|||
/** 是否显示标记点 */ |
|||
showMarkers?: boolean |
|||
/** 是否显示围栏 */ |
|||
showFences?: boolean |
|||
/** 是否显示绘制围栏 */ |
|||
showDrawFences?: boolean |
|||
} |
|||
|
|||
// 围栏数据接口
|
|||
export interface FenceData { |
|||
/** 围栏ID */ |
|||
id: string |
|||
/** 围栏名称 */ |
|||
name: string |
|||
/** 围栏范围 */ |
|||
fenceRange: [number, number][] |
|||
/** 围栏状态 */ |
|||
status: number |
|||
/** 围栏类型 */ |
|||
type: number |
|||
/** 围栏备注 */ |
|||
remark: string |
|||
/** 围栏数据 */ |
|||
data: any |
|||
} |
|||
|
|||
// 探测器信息接口
|
|||
export interface DetectorInfo { |
|||
name: string |
|||
status: string |
|||
statusLabel: string |
|||
statusColor: string |
|||
} |
|||
|
|||
// 轨迹点数据接口
|
|||
export interface TrajectoryPoint { |
|||
/** 设备ID */ |
|||
deviceId?: number |
|||
/** 轨迹点索引 */ |
|||
pointIndex?: number |
|||
/** 坐标 [经度, 纬度] */ |
|||
coordinates: [number, number] |
|||
/** 时间戳 */ |
|||
timestamp: number |
|||
/** 速度 (km/h) */ |
|||
speed?: number |
|||
/** 方向 (度) */ |
|||
direction?: number |
|||
/** 额外数据 */ |
|||
data?: any |
|||
} |
|||
|
|||
// 轨迹数据接口
|
|||
export interface TrajectoryData { |
|||
/** 设备ID */ |
|||
deviceId: number |
|||
/** 轨迹点列表 */ |
|||
points: TrajectoryPoint[] |
|||
/** 轨迹颜色 */ |
|||
color?: string |
|||
/** 轨迹线宽 */ |
|||
width?: number |
|||
/** 设备名称 */ |
|||
name?: string |
|||
/** 轨迹点半径 */ |
|||
pointRadius?: number |
|||
/** 激活状态轨迹点半径 */ |
|||
activePointRadius?: number |
|||
/** 是否显示时间标签 */ |
|||
showTimeLabel?: boolean |
|||
} |
|||
|
|||
// 轨迹播放状态
|
|||
export interface TrajectoryPlayState { |
|||
/** 是否正在播放 */ |
|||
isPlaying: boolean |
|||
/** 当前播放时间 */ |
|||
currentTime: number |
|||
/** 播放速度倍数 */ |
|||
speed: number |
|||
/** 播放开始时间 */ |
|||
startTime?: number |
|||
/** 播放结束时间 */ |
|||
endTime?: number |
|||
} |
|||
|
|||
// 地图实例接口
|
|||
export interface MapInstance { |
|||
map: any |
|||
tileLayer: any |
|||
markerLayer: any |
|||
rippleLayer: any |
|||
popupOverlay: any |
|||
trajectoryLayer?: any |
|||
} |
@ -0,0 +1,202 @@ |
|||
/** |
|||
* 地图工具函数 |
|||
*/ |
|||
import { Style, Text, Circle, Fill, Stroke, Icon } from 'ol/style' |
|||
import { Feature } from 'ol' |
|||
import type { MarkerData, DetectorInfo } from '../types/map.types' |
|||
import { STATUS_DICT, STATUS_PRIORITY, STATUS_ORDER } from '../constants/map.constants' |
|||
|
|||
/** |
|||
* 从字典中查找状态信息 |
|||
*/ |
|||
export const findStatusInfo = ( |
|||
dict: (typeof STATUS_DICT)[keyof typeof STATUS_DICT], |
|||
value: string |
|||
) => { |
|||
return dict.find((item) => item.value === value) |
|||
} |
|||
|
|||
/** |
|||
* 获取状态映射 |
|||
*/ |
|||
export const getStatusMapping = (type: keyof typeof STATUS_DICT, value: string) => { |
|||
const info = findStatusInfo(STATUS_DICT[type], value) |
|||
return info ? `${type}_${value}` : null |
|||
} |
|||
|
|||
/** |
|||
* 根据字典数据获取设备最高优先级状态 |
|||
*/ |
|||
export const getHighestPriorityStatus = (markerData: MarkerData): keyof typeof STATUS_PRIORITY => { |
|||
const statuses: Array<keyof typeof STATUS_PRIORITY> = [] |
|||
|
|||
// 检查各种状态
|
|||
const gasStatus = getStatusMapping('gas', markerData.gasStatus) |
|||
const batteryStatus = getStatusMapping('battery', markerData.batteryStatus) |
|||
const fenceStatus = getStatusMapping('fence', markerData.fenceStatus) |
|||
const onlineStatus = markerData.onlineStatus === '0' ? 'offline' : null |
|||
|
|||
// 收集非正常状态
|
|||
if (gasStatus && markerData.gasStatus !== '0') |
|||
statuses.push(gasStatus as keyof typeof STATUS_PRIORITY) |
|||
if (batteryStatus && markerData.batteryStatus !== '0') |
|||
statuses.push(batteryStatus as keyof typeof STATUS_PRIORITY) |
|||
if (fenceStatus && markerData.fenceStatus !== '0') |
|||
statuses.push(fenceStatus as keyof typeof STATUS_PRIORITY) |
|||
if (onlineStatus) statuses.push(onlineStatus) |
|||
|
|||
// 如果没有报警状态,则为正常
|
|||
if (statuses.length === 0) return 'normal' |
|||
|
|||
// 返回优先级最高的状态
|
|||
return statuses.reduce((prev, current) => |
|||
STATUS_PRIORITY[prev] < STATUS_PRIORITY[current] ? prev : current |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 根据字典数据获取状态颜色 |
|||
*/ |
|||
export const getStatusColor = (status: keyof typeof STATUS_PRIORITY): string => { |
|||
if (status === 'normal') return '#67c23a' |
|||
if (status === 'offline') return STATUS_DICT.online[0].cssClass |
|||
|
|||
const [type, value] = status.split('_') as [keyof typeof STATUS_DICT, string] |
|||
const info = findStatusInfo(STATUS_DICT[type], value) |
|||
return info?.cssClass || '#67c23a' |
|||
} |
|||
|
|||
/** |
|||
* 根据字典数据获取状态标签 |
|||
*/ |
|||
export const getStatusLabel = (status: keyof typeof STATUS_PRIORITY): string => { |
|||
if (status === 'normal') return '正常' |
|||
if (status === 'offline') return STATUS_DICT.online[0].label |
|||
|
|||
const [type, value] = status.split('_') as [keyof typeof STATUS_DICT, string] |
|||
const info = findStatusInfo(STATUS_DICT[type], value) |
|||
return info?.label || '正常' |
|||
} |
|||
|
|||
/** |
|||
* 创建位置图标SVG |
|||
*/ |
|||
export const createLocationIconSVG = (color: string, size: number = 24) => { |
|||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(` |
|||
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="${color}"/> |
|||
<circle cx="12" cy="9" r="2" fill="white"/> |
|||
</svg> |
|||
`)}` |
|||
} |
|||
|
|||
/** |
|||
* 创建标记样式 |
|||
*/ |
|||
export const createMarkerStyle = ( |
|||
markerData: MarkerData | keyof typeof STATUS_PRIORITY, |
|||
isCluster: boolean = false, |
|||
clusterSize?: number |
|||
) => { |
|||
// 如果是字符串,说明是状态值
|
|||
const status: keyof typeof STATUS_PRIORITY = |
|||
typeof markerData === 'string' |
|||
? (markerData as keyof typeof STATUS_PRIORITY) |
|||
: getHighestPriorityStatus(markerData) |
|||
|
|||
const color = getStatusColor(status) |
|||
|
|||
if (isCluster && clusterSize) { |
|||
// 聚合标记样式
|
|||
return new Style({ |
|||
image: new Circle({ |
|||
radius: Math.min(20 + clusterSize * 2, 40), |
|||
fill: new Fill({ |
|||
color: color + '80' // 添加透明度
|
|||
}), |
|||
stroke: new Stroke({ |
|||
color: color, |
|||
width: 2 |
|||
}) |
|||
}), |
|||
text: new Text({ |
|||
text: clusterSize.toString(), |
|||
fill: new Fill({ |
|||
color: '#ffffff' |
|||
}), |
|||
font: 'bold 14px Arial' |
|||
}) |
|||
}) |
|||
} else { |
|||
// 单个标记样式 - 使用位置图标
|
|||
return new Style({ |
|||
image: new Icon({ |
|||
src: createLocationIconSVG(color, 24), |
|||
scale: 1, |
|||
anchor: [0.5, 1], // 锚点设置在底部中心
|
|||
anchorXUnits: 'fraction', |
|||
anchorYUnits: 'fraction' |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 生成探测器列表项HTML |
|||
*/ |
|||
export const createDetectorListItem = (detector: DetectorInfo) => ` |
|||
<div style="display: flex; align-items: center; padding: 6px 0; border-bottom: 1px solid #f0f0f0;"> |
|||
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${detector.statusColor}; margin-right: 10px; flex-shrink: 0;"></div> |
|||
<div style="flex: 1; min-width: 0;"> |
|||
<div style="font-weight: 500; font-size: 13px; color: #333; margin-bottom: 2px;">${detector.name}</div> |
|||
<div style="color: ${detector.statusColor}; font-size: 11px; font-weight: 400;">${detector.statusLabel}</div> |
|||
</div> |
|||
</div> |
|||
` |
|||
|
|||
/** |
|||
* 生成聚合标记弹窗HTML |
|||
*/ |
|||
export const createClusterPopupHTML = (detectorList: DetectorInfo[]) => { |
|||
const detectorListHTML = detectorList.map(createDetectorListItem).join('') |
|||
|
|||
return ` |
|||
<div style="max-height: 250px; overflow-y: auto; padding-right: 4px;"> |
|||
${detectorListHTML} |
|||
</div> |
|||
` |
|||
} |
|||
|
|||
/** |
|||
* 获取聚合标记数据 |
|||
*/ |
|||
export const getClusterMarkerData = (features: Feature[]): keyof typeof STATUS_PRIORITY => { |
|||
// 收集所有标记的状态
|
|||
const allStatuses: Array<keyof typeof STATUS_PRIORITY> = [] |
|||
|
|||
features.forEach((feature) => { |
|||
const markerData = feature.get('markerData') as MarkerData |
|||
if (markerData) { |
|||
const status = getHighestPriorityStatus(markerData) |
|||
allStatuses.push(status) |
|||
} |
|||
}) |
|||
|
|||
// 返回优先级最高的状态
|
|||
if (allStatuses.length === 0) return 'normal' |
|||
|
|||
return allStatuses.reduce((prev, current) => |
|||
STATUS_PRIORITY[prev] < STATUS_PRIORITY[current] ? prev : current |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 按优先级排序探测器列表 |
|||
*/ |
|||
export const sortDetectorsByPriority = (detectorList: DetectorInfo[]): DetectorInfo[] => { |
|||
return detectorList.sort((a, b) => { |
|||
const aPriority = STATUS_ORDER.indexOf(a.status as keyof typeof STATUS_PRIORITY) |
|||
const bPriority = STATUS_ORDER.indexOf(b.status as keyof typeof STATUS_PRIORITY) |
|||
return aPriority - bPriority |
|||
}) |
|||
} |
@ -0,0 +1,37 @@ |
|||
<template> |
|||
<OpenLayerMap v-if="inited" :markers="markers" /> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import OpenLayerMap from './components/OpenLayerMap.vue' |
|||
import { getLastestDetectorData } from '@/api/gas' |
|||
import { HandDetector } from '@/api/gas/handdetector' |
|||
import { MarkerData } from './components/types/map.types' |
|||
const getDataTimer = ref<NodeJS.Timeout | null>(null) |
|||
const markers = ref<MarkerData[]>([]) |
|||
const inited = ref(false) |
|||
const getMarkers = async () => { |
|||
console.log('getMarkers') |
|||
return await getLastestDetectorData().then((res: HandDetector[]) => { |
|||
res = res.filter((i) => i.enableStatus === 1) |
|||
res = res.map((i) => { |
|||
return { |
|||
...i, |
|||
coordinates: [i.longitude, i.latitude], |
|||
data: [] |
|||
} |
|||
}) |
|||
markers.value = res as unknown as any[] |
|||
inited.value = true |
|||
}) |
|||
} |
|||
onMounted(() => { |
|||
getMarkers() |
|||
getDataTimer.value = setInterval(() => { |
|||
getMarkers() |
|||
}, 5000) |
|||
}) |
|||
onUnmounted(() => { |
|||
clearInterval(getDataTimer.value as NodeJS.Timeout) |
|||
}) |
|||
</script> |
|||
<style scoped></style> |
@ -1,31 +1,7 @@ |
|||
<template> |
|||
<div> |
|||
<el-card shadow="never"> |
|||
<el-skeleton :loading="loading" animated> |
|||
<el-row :gutter="16" justify="space-between"> |
|||
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24"> |
|||
<div class="flex items-center"> |
|||
<el-avatar :src="avatar" :size="70" class="mr-16px"> |
|||
<img src="@/assets/imgs/user.png" alt="" /> |
|||
</el-avatar> |
|||
<div> |
|||
<div class="text-20px"> |
|||
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</el-col> |
|||
</el-row> |
|||
</el-skeleton> |
|||
</el-card> |
|||
</div> |
|||
<HandDeviceHome /> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { useUserStore } from '@/store/modules/user' |
|||
import HandDeviceHome from '../HandDevice/Home/index.vue' |
|||
defineOptions({ name: 'Index' }) |
|||
const { t } = useI18n() |
|||
const userStore = useUserStore() |
|||
const loading = ref(false) |
|||
const avatar = userStore.getUser.avatar |
|||
const username = userStore.getUser.nickname |
|||
</script> |
|||
|
@ -0,0 +1,204 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="120px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="气体类型" prop="gasTypeId"> |
|||
<el-select v-model="formData.gasTypeId" placeholder="请选择气体类型"> |
|||
<el-option |
|||
v-for="item in props.gasTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="警报类型" prop="alarmTypeId"> |
|||
<el-select |
|||
v-model="formData.alarmTypeId" |
|||
placeholder="请选择警报类型" |
|||
@change="handleAlarmTypeIdChange" |
|||
> |
|||
<el-option |
|||
v-for="item in props.alarmTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="警报名称" prop="alarmName"> |
|||
<el-input v-model="formData.alarmName" placeholder="请输入警报名称" /> |
|||
</el-form-item> |
|||
<el-form-item label="警报名称颜色" prop="alarmNameColor"> |
|||
<el-color-picker v-model="formData.alarmNameColor" :disabled="true" /> |
|||
</el-form-item> |
|||
<el-form-item label="警报颜色" prop="alarmColor"> |
|||
<el-color-picker |
|||
v-model="formData.alarmColor" |
|||
placeholder="请输入警报颜色" |
|||
:disabled="true" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="警报方式/级别" prop="alarmLevel"> |
|||
<el-select v-model="formData.alarmLevel" placeholder="请输入警报方式/级别" :disabled="true"> |
|||
<el-option |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_ALARM_LEVEL)" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="触发值(小)" prop="min"> |
|||
<el-input-number |
|||
v-model="formData.min" |
|||
placeholder="请输入触发值(小)" |
|||
:controls="false" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="触发值(大)" prop="max"> |
|||
<el-input-number |
|||
v-model="formData.max" |
|||
placeholder="请输入触发值(大)" |
|||
:controls="false" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="最值方向" prop="direction"> |
|||
<el-select v-model="formData.direction" placeholder="请输入最值方向"> |
|||
<el-option |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_VALUE_DIRECTION)" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="排序" prop="sortOrder"> |
|||
<el-input v-model="formData.sortOrder" placeholder="请输入排序" /> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { AlarmRuleApi, AlarmRule } from '@/api/gas/alarmrule' |
|||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' |
|||
/** GAS警报规则 表单 */ |
|||
defineOptions({ name: 'AlarmRuleForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
const props = defineProps({ |
|||
gasTypes: { |
|||
type: Array as PropType<any[]>, |
|||
required: true |
|||
}, |
|||
alarmTypes: { |
|||
type: Array as PropType<any[]>, |
|||
required: true |
|||
} |
|||
}) |
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const formData = ref({ |
|||
id: undefined, |
|||
gasTypeId: undefined, |
|||
alarmTypeId: undefined, |
|||
alarmName: undefined, |
|||
alarmNameColor: undefined, |
|||
alarmColor: undefined, |
|||
alarmLevel: undefined, |
|||
min: undefined, |
|||
max: undefined, |
|||
direction: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
gasTypeId: [{ required: true, message: '气体类型不能为空', trigger: 'blur' }], |
|||
alarmTypeId: [{ required: true, message: '警报类型不能为空', trigger: 'blur' }], |
|||
alarmName: [{ required: true, message: '警报名称不能为空', trigger: 'blur' }], |
|||
direction: [{ required: true, message: '最值方向不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await AlarmRuleApi.getAlarmRule(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as AlarmRule |
|||
if (formType.value === 'create') { |
|||
await AlarmRuleApi.createAlarmRule(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await AlarmRuleApi.updateAlarmRule(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
gasTypeId: undefined, |
|||
alarmTypeId: undefined, |
|||
alarmName: undefined, |
|||
alarmNameColor: undefined, |
|||
alarmColor: undefined, |
|||
alarmLevel: undefined, |
|||
min: undefined, |
|||
max: undefined, |
|||
direction: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
|
|||
/** 警报类型改变 */ |
|||
const handleAlarmTypeIdChange = (value: number) => { |
|||
formData.value.alarmName = props.alarmTypes.find((item) => item.id === value)?.name |
|||
formData.value.alarmNameColor = props.alarmTypes.find((item) => item.id === value)?.nameColor |
|||
} |
|||
</script> |
@ -0,0 +1,268 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="120px" |
|||
> |
|||
<el-form-item label="气体类型" prop="gasTypeId"> |
|||
<el-select |
|||
v-model="queryParams.gasTypeId" |
|||
placeholder="请选择气体类型" |
|||
clearable |
|||
filterable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getGasTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="警报类型" prop="alarmTypeId"> |
|||
<el-select |
|||
v-model="queryParams.alarmTypeId" |
|||
placeholder="请选择警报类型" |
|||
filterable |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getAlarmTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:alarm-rule:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:alarm-rule:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:alarm-rule:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="气体类型" align="center" prop="gasTypeId"> |
|||
<template #default="scope"> |
|||
{{ handDetectorStore.getGasTypes.find((item) => item.id === scope.row.gasTypeId)?.name }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="警报类型" align="center" prop="alarmTypeId"> |
|||
<template #default="scope"> |
|||
{{ |
|||
handDetectorStore.getAlarmTypes.find((item) => item.id === scope.row.alarmTypeId)?.name |
|||
}} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="警报名称" align="center" prop="alarmName" /> |
|||
<el-table-column label="警报方式/级别" align="center" prop="alarmLevel" /> |
|||
<el-table-column label="触发值(小)" align="center" prop="min" /> |
|||
<el-table-column label="触发值(大)" align="center" prop="max" /> |
|||
<el-table-column label="最值方向" align="center" prop="direction" /> |
|||
<el-table-column label="排序" align="center" prop="sortOrder" /> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:alarm-rule:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:alarm-rule:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<AlarmRuleForm |
|||
ref="formRef" |
|||
@success="getList" |
|||
:gasTypes="handDetectorStore.getGasTypes" |
|||
:alarmTypes="handDetectorStore.getAlarmTypes" |
|||
/> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { dateFormatter } from '@/utils/formatTime' |
|||
import download from '@/utils/download' |
|||
import { AlarmRuleApi, AlarmRule } from '@/api/gas/alarmrule' |
|||
import AlarmRuleForm from './AlarmRuleForm.vue' |
|||
import { useHandDetectorStore } from '@/store/modules/handDetector' |
|||
|
|||
/** GAS警报规则 列表 */ |
|||
defineOptions({ name: 'AlarmRule' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
const handDetectorStore = useHandDetectorStore() |
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<AlarmRule[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
gasTypeId: undefined, |
|||
alarmTypeId: undefined, |
|||
alarmName: undefined, |
|||
alarmNameColor: undefined, |
|||
alarmColor: undefined, |
|||
alarmLevel: undefined, |
|||
min: undefined, |
|||
max: undefined, |
|||
direction: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined, |
|||
createTime: [] |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await AlarmRuleApi.getAlarmRulePage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await AlarmRuleApi.deleteAlarmRule(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS警报规则 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await AlarmRuleApi.deleteAlarmRuleList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: AlarmRule[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await AlarmRuleApi.exportAlarmRule(queryParams) |
|||
download.excel(data, 'GAS警报规则.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
}) |
|||
</script> |
@ -0,0 +1,136 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="120px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="名称" prop="name"> |
|||
<el-input v-model="formData.name" placeholder="请输入名称" /> |
|||
</el-form-item> |
|||
<el-form-item label="名称颜色" prop="nameColor"> |
|||
<el-color-picker v-model="formData.nameColor" /> |
|||
</el-form-item> |
|||
<el-form-item label="颜色" prop="color"> |
|||
<el-color-picker v-model="formData.color" /> |
|||
</el-form-item> |
|||
<el-form-item label="警报方式/级别" prop="level"> |
|||
<el-select v-model="formData.level" placeholder="请选择警报方式/级别"> |
|||
<el-option |
|||
v-for="item in props.alarmLevels" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="排序" prop="sortOrder"> |
|||
<el-input v-model="formData.sortOrder" placeholder="请输入排序" /> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { AlarmTypeApi, AlarmType } from '@/api/gas/alarmtype' |
|||
/** GAS警报类型 表单 */ |
|||
defineOptions({ name: 'AlarmTypeForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
const props = defineProps({ |
|||
alarmLevels: { |
|||
type: Array as PropType<any[]>, |
|||
required: true |
|||
} |
|||
}) |
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const formData = ref({ |
|||
id: undefined, |
|||
name: undefined, |
|||
nameColor: '#000', |
|||
color: undefined, |
|||
level: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], |
|||
color: [{ required: true, message: '颜色不能为空', trigger: 'blur' }], |
|||
level: [ |
|||
{ |
|||
required: true, |
|||
message: '警报方式/级别不能为空', |
|||
trigger: 'blur' |
|||
} |
|||
] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await AlarmTypeApi.getAlarmType(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as AlarmType |
|||
if (formType.value === 'create') { |
|||
await AlarmTypeApi.createAlarmType(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await AlarmTypeApi.updateAlarmType(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
name: undefined, |
|||
nameColor: '#000', |
|||
color: undefined, |
|||
level: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
</script> |
@ -0,0 +1,237 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="120px" |
|||
> |
|||
<el-form-item label="名称" prop="name"> |
|||
<el-input |
|||
v-model="queryParams.name" |
|||
placeholder="请输入名称" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="警报方式/级别" prop="level"> |
|||
<el-input |
|||
v-model="queryParams.level" |
|||
placeholder="请输入警报方式/级别" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:alarm-type:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:alarm-type:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:alarm-type:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="名称" align="center" prop="name" /> |
|||
<el-table-column label="图例" align="center" |
|||
><template #default="scope"> |
|||
<div class="flex items-center"> |
|||
<div |
|||
class="w-4px h-4px rounded-full" |
|||
:style="{ backgroundColor: scope.row.color, color: scope.row.nameColor || '#000' }" |
|||
> |
|||
{{ getDictLabel(DICT_TYPE.HAND_DETECTOR_ALARM_LEVEL, scope.row.level as number) }} |
|||
</div> |
|||
</div> |
|||
</template></el-table-column |
|||
> |
|||
<el-table-column label="警报方式/级别" align="center" prop="level" /> |
|||
<el-table-column label="排序" align="center" prop="sortOrder" /> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:alarm-type:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:alarm-type:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<AlarmTypeForm |
|||
ref="formRef" |
|||
@success="getList" |
|||
:alarmLevels="getIntDictOptions(DICT_TYPE.HAND_DETECTOR_ALARM_LEVEL)" |
|||
/> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { dateFormatter } from '@/utils/formatTime' |
|||
import download from '@/utils/download' |
|||
import { AlarmTypeApi, AlarmType } from '@/api/gas/alarmtype' |
|||
import AlarmTypeForm from './AlarmTypeForm.vue' |
|||
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict' |
|||
/** GAS警报类型 列表 */ |
|||
defineOptions({ name: 'AlarmType' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
|
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<AlarmType[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
name: undefined, |
|||
level: undefined, |
|||
remark: undefined |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await AlarmTypeApi.getAlarmTypePage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await AlarmTypeApi.deleteAlarmType(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS警报类型 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await AlarmTypeApi.deleteAlarmTypeList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: AlarmType[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await AlarmTypeApi.exportAlarmType(queryParams) |
|||
download.excel(data, 'GAS警报类型.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
}) |
|||
</script> |
@ -0,0 +1,190 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="100px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="父节点ID" prop="parentId"> |
|||
<el-input v-model="formData.parentId" placeholder="请输入父节点ID" /> |
|||
</el-form-item> |
|||
<el-form-item label="层级(1:工厂;2:车间;3:班组)" prop="type"> |
|||
<el-select v-model="formData.type" placeholder="请选择层级(1:工厂;2:车间;3:班组)"> |
|||
<el-option label="请选择字典生成" value="" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="名称" prop="name"> |
|||
<el-input v-model="formData.name" placeholder="请输入名称" /> |
|||
</el-form-item> |
|||
<el-form-item label="城市" prop="city"> |
|||
<el-input v-model="formData.city" placeholder="请输入城市" /> |
|||
</el-form-item> |
|||
<el-form-item label="总警报数" prop="alarmTotal"> |
|||
<el-input v-model="formData.alarmTotal" placeholder="请输入总警报数" /> |
|||
</el-form-item> |
|||
<el-form-item label="已处理警报数" prop="alarmDeal"> |
|||
<el-input v-model="formData.alarmDeal" placeholder="请输入已处理警报数" /> |
|||
</el-form-item> |
|||
<el-form-item label="区域图" prop="picUrl"> |
|||
<el-input v-model="formData.picUrl" placeholder="请输入区域图" /> |
|||
</el-form-item> |
|||
<el-form-item label="区域图缩放比例" prop="picScale"> |
|||
<el-input v-model="formData.picScale" placeholder="请输入区域图缩放比例" /> |
|||
</el-form-item> |
|||
<el-form-item label="在区域图X坐标值" prop="picX"> |
|||
<el-input v-model="formData.picX" placeholder="请输入在区域图X坐标值" /> |
|||
</el-form-item> |
|||
<el-form-item label="在区域图X坐标值" prop="picY"> |
|||
<el-input v-model="formData.picY" placeholder="请输入在区域图X坐标值" /> |
|||
</el-form-item> |
|||
<el-form-item label="经度" prop="longitude"> |
|||
<el-input v-model="formData.longitude" placeholder="请输入经度" /> |
|||
</el-form-item> |
|||
<el-form-item label="纬度" prop="latitude"> |
|||
<el-input v-model="formData.latitude" placeholder="请输入纬度" /> |
|||
</el-form-item> |
|||
<el-form-item label="区域西南坐标" prop="rectSouthWest"> |
|||
<el-input v-model="formData.rectSouthWest" placeholder="请输入区域西南坐标" /> |
|||
</el-form-item> |
|||
<el-form-item label="区域东北坐标" prop="rectNorthEast"> |
|||
<el-input v-model="formData.rectNorthEast" placeholder="请输入区域东北坐标" /> |
|||
</el-form-item> |
|||
<el-form-item label="排序" prop="sortOrder"> |
|||
<el-input v-model="formData.sortOrder" placeholder="请输入排序" /> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
<el-form-item label="删除标志" prop="delFlag"> |
|||
<el-input v-model="formData.delFlag" placeholder="请输入删除标志" /> |
|||
</el-form-item> |
|||
<el-form-item label="创建者" prop="createBy"> |
|||
<el-input v-model="formData.createBy" placeholder="请输入创建者" /> |
|||
</el-form-item> |
|||
<el-form-item label="更新者" prop="updateBy"> |
|||
<el-input v-model="formData.updateBy" placeholder="请输入更新者" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { FactoryApi, Factory } from '@/api/gas/factory' |
|||
|
|||
/** GAS工厂 表单 */ |
|||
defineOptions({ name: 'FactoryForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
|
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const formData = ref({ |
|||
id: undefined, |
|||
parentId: undefined, |
|||
type: undefined, |
|||
name: undefined, |
|||
city: undefined, |
|||
alarmTotal: undefined, |
|||
alarmDeal: undefined, |
|||
picUrl: undefined, |
|||
picScale: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
longitude: undefined, |
|||
latitude: undefined, |
|||
rectSouthWest: undefined, |
|||
rectNorthEast: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined, |
|||
delFlag: undefined, |
|||
createBy: undefined, |
|||
updateBy: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
parentId: [{ required: true, message: '父节点ID不能为空', trigger: 'blur' }], |
|||
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], |
|||
alarmTotal: [{ required: true, message: '总警报数不能为空', trigger: 'blur' }], |
|||
alarmDeal: [{ required: true, message: '已处理警报数不能为空', trigger: 'blur' }], |
|||
sortOrder: [{ required: true, message: '排序不能为空', trigger: 'blur' }], |
|||
delFlag: [{ required: true, message: '删除标志不能为空', trigger: 'blur' }], |
|||
createBy: [{ required: true, message: '创建者不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await FactoryApi.getFactory(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as Factory |
|||
if (formType.value === 'create') { |
|||
await FactoryApi.createFactory(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await FactoryApi.updateFactory(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
parentId: undefined, |
|||
type: undefined, |
|||
name: undefined, |
|||
city: undefined, |
|||
alarmTotal: undefined, |
|||
alarmDeal: undefined, |
|||
picUrl: undefined, |
|||
picScale: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
longitude: undefined, |
|||
latitude: undefined, |
|||
rectSouthWest: undefined, |
|||
rectNorthEast: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined, |
|||
delFlag: undefined, |
|||
createBy: undefined, |
|||
updateBy: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
</script> |
@ -0,0 +1,400 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="68px" |
|||
> |
|||
<el-form-item label="父节点ID" prop="parentId"> |
|||
<el-input |
|||
v-model="queryParams.parentId" |
|||
placeholder="请输入父节点ID" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="层级(1:工厂;2:车间;3:班组)" prop="type"> |
|||
<el-select |
|||
v-model="queryParams.type" |
|||
placeholder="请选择层级(1:工厂;2:车间;3:班组)" |
|||
clearable |
|||
class="!w-240px" |
|||
> |
|||
<el-option label="请选择字典生成" value="" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="名称" prop="name"> |
|||
<el-input |
|||
v-model="queryParams.name" |
|||
placeholder="请输入名称" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="城市" prop="city"> |
|||
<el-input |
|||
v-model="queryParams.city" |
|||
placeholder="请输入城市" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="总警报数" prop="alarmTotal"> |
|||
<el-input |
|||
v-model="queryParams.alarmTotal" |
|||
placeholder="请输入总警报数" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="已处理警报数" prop="alarmDeal"> |
|||
<el-input |
|||
v-model="queryParams.alarmDeal" |
|||
placeholder="请输入已处理警报数" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="区域图" prop="picUrl"> |
|||
<el-input |
|||
v-model="queryParams.picUrl" |
|||
placeholder="请输入区域图" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="区域图缩放比例" prop="picScale"> |
|||
<el-input |
|||
v-model="queryParams.picScale" |
|||
placeholder="请输入区域图缩放比例" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="在区域图X坐标值" prop="picX"> |
|||
<el-input |
|||
v-model="queryParams.picX" |
|||
placeholder="请输入在区域图X坐标值" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="在区域图X坐标值" prop="picY"> |
|||
<el-input |
|||
v-model="queryParams.picY" |
|||
placeholder="请输入在区域图X坐标值" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="经度" prop="longitude"> |
|||
<el-input |
|||
v-model="queryParams.longitude" |
|||
placeholder="请输入经度" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="纬度" prop="latitude"> |
|||
<el-input |
|||
v-model="queryParams.latitude" |
|||
placeholder="请输入纬度" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="区域西南坐标" prop="rectSouthWest"> |
|||
<el-input |
|||
v-model="queryParams.rectSouthWest" |
|||
placeholder="请输入区域西南坐标" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="区域东北坐标" prop="rectNorthEast"> |
|||
<el-input |
|||
v-model="queryParams.rectNorthEast" |
|||
placeholder="请输入区域东北坐标" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="排序" prop="sortOrder"> |
|||
<el-input |
|||
v-model="queryParams.sortOrder" |
|||
placeholder="请输入排序" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input |
|||
v-model="queryParams.remark" |
|||
placeholder="请输入备注" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="删除标志" prop="delFlag"> |
|||
<el-input |
|||
v-model="queryParams.delFlag" |
|||
placeholder="请输入删除标志" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="创建者" prop="createBy"> |
|||
<el-input |
|||
v-model="queryParams.createBy" |
|||
placeholder="请输入创建者" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:factory:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:factory:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:factory:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="主键ID" align="center" prop="id" /> |
|||
<el-table-column label="父节点ID" align="center" prop="parentId" /> |
|||
<el-table-column label="层级(1:工厂;2:车间;3:班组)" align="center" prop="type" /> |
|||
<el-table-column label="名称" align="center" prop="name" /> |
|||
<el-table-column label="城市" align="center" prop="city" /> |
|||
<el-table-column label="总警报数" align="center" prop="alarmTotal" /> |
|||
<el-table-column label="已处理警报数" align="center" prop="alarmDeal" /> |
|||
<el-table-column label="区域图" align="center" prop="picUrl" /> |
|||
<el-table-column label="区域图缩放比例" align="center" prop="picScale" /> |
|||
<el-table-column label="在区域图X坐标值" align="center" prop="picX" /> |
|||
<el-table-column label="在区域图X坐标值" align="center" prop="picY" /> |
|||
<el-table-column label="经度" align="center" prop="longitude" /> |
|||
<el-table-column label="纬度" align="center" prop="latitude" /> |
|||
<el-table-column label="区域西南坐标" align="center" prop="rectSouthWest" /> |
|||
<el-table-column label="区域东北坐标" align="center" prop="rectNorthEast" /> |
|||
<el-table-column label="排序" align="center" prop="sortOrder" /> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column label="删除标志" align="center" prop="delFlag" /> |
|||
<el-table-column label="创建者" align="center" prop="createBy" /> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="更新者" align="center" prop="updateBy" /> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:factory:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:factory:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<FactoryForm ref="formRef" @success="getList" /> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { dateFormatter } from '@/utils/formatTime' |
|||
import download from '@/utils/download' |
|||
import { FactoryApi, Factory } from '@/api/gas/factory' |
|||
import FactoryForm from './FactoryForm.vue' |
|||
|
|||
/** GAS工厂 列表 */ |
|||
defineOptions({ name: 'Factory' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
|
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<Factory[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
parentId: undefined, |
|||
type: undefined, |
|||
name: undefined, |
|||
city: undefined, |
|||
alarmTotal: undefined, |
|||
alarmDeal: undefined, |
|||
picUrl: undefined, |
|||
picScale: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
longitude: undefined, |
|||
latitude: undefined, |
|||
rectSouthWest: undefined, |
|||
rectNorthEast: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined, |
|||
delFlag: undefined, |
|||
createBy: undefined, |
|||
createTime: [], |
|||
updateBy: undefined |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await FactoryApi.getFactoryPage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await FactoryApi.deleteFactory(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS工厂 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await FactoryApi.deleteFactoryList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: Factory[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await FactoryApi.exportFactory(queryParams) |
|||
download.excel(data, 'GAS工厂.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
}) |
|||
</script> |
@ -0,0 +1,132 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="100px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="围栏名称" prop="name"> |
|||
<el-input v-model="formData.name" placeholder="请输入围栏名称" /> |
|||
</el-form-item> |
|||
<el-form-item label="围栏范围" prop="fenceRange"> |
|||
<el-input v-model="formData.fenceRange" placeholder="请输入围栏范围" /> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-radio-group v-model="formData.status"> |
|||
<el-radio-button |
|||
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS)" |
|||
:key="dict.value" |
|||
:value="dict.value" |
|||
> |
|||
{{ dict.label }} |
|||
</el-radio-button> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="围栏类型" prop="type"> |
|||
<el-radio-group v-model="formData.type"> |
|||
<el-radio-button |
|||
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE)" |
|||
:key="dict.value" |
|||
:value="dict.value" |
|||
> |
|||
{{ dict.label }} |
|||
</el-radio-button> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { FenceApi, Fence } from '@/api/gas/fence' |
|||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
|||
|
|||
/** GAS电子围栏 表单 */ |
|||
defineOptions({ name: 'FenceForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
|
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const formData = ref({ |
|||
id: undefined, |
|||
name: undefined, |
|||
fenceRange: undefined, |
|||
status: 1, |
|||
type: 1, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
name: [{ required: true, message: '围栏名称不能为空', trigger: 'blur' }], |
|||
fenceRange: [{ required: true, message: '围栏范围不能为空', trigger: 'blur' }], |
|||
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }], |
|||
type: [{ required: true, message: '围栏类型不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await FenceApi.getFence(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as Fence |
|||
if (formType.value === 'create') { |
|||
await FenceApi.createFence(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await FenceApi.updateFence(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
name: undefined, |
|||
fenceRange: undefined, |
|||
status: 1, |
|||
type: 1, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
</script> |
@ -0,0 +1,243 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="68px" |
|||
> |
|||
<el-form-item label="围栏名称" prop="name"> |
|||
<el-input |
|||
v-model="queryParams.name" |
|||
placeholder="请输入围栏名称" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> |
|||
<el-option |
|||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" |
|||
:key="dict.value" |
|||
:label="dict.label" |
|||
:value="dict.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="围栏类型" prop="type"> |
|||
<el-select |
|||
v-model="queryParams.type" |
|||
placeholder="请选择围栏类型" |
|||
clearable |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE)" |
|||
:key="dict.value" |
|||
:label="dict.label" |
|||
:value="dict.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:fence:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:fence:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:fence:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="围栏名称" align="center" prop="name" /> |
|||
<el-table-column label="状态" align="center" prop="status"> |
|||
<template #default="scope"> |
|||
<DictTag :type="DICT_TYPE.HAND_DETECTOR_FENCE_STATUS" :value="scope.row.status" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="围栏类型" align="center" prop="type"> |
|||
<template #default="scope"> |
|||
<DictTag :type="DICT_TYPE.HAND_DETECTOR_FENCE_TYPE" :value="scope.row.type" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
|
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:fence:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:fence:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<FenceForm ref="formRef" @success="getList" /> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
|||
import download from '@/utils/download' |
|||
import { FenceApi, Fence } from '@/api/gas/fence' |
|||
import FenceForm from './FenceForm.vue' |
|||
|
|||
/** GAS电子围栏 列表 */ |
|||
defineOptions({ name: 'Fence' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
|
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<Fence[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
name: undefined, |
|||
fenceRange: undefined, |
|||
status: undefined, |
|||
type: undefined, |
|||
remark: undefined, |
|||
createTime: [] |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await FenceApi.getFencePage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await FenceApi.deleteFence(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS电子围栏 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await FenceApi.deleteFenceList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: Fence[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await FenceApi.exportFence(queryParams) |
|||
download.excel(data, 'GAS电子围栏.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
}) |
|||
</script> |
@ -0,0 +1,194 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="100px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="持有人" prop="detectorId"> |
|||
<el-select v-model="formData.detectorId" placeholder="请选择持有人"> |
|||
<el-option |
|||
v-for="item in props.handDetector" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="围栏" prop="fenceId"> |
|||
<el-select v-model="formData.fenceId" placeholder="请选择围栏"> |
|||
<el-option |
|||
v-for="item in props.fences" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="报警类型" prop="type"> |
|||
<el-select v-model="formData.type" placeholder="请选择报警类型"> |
|||
<el-option |
|||
v-for="item in props.alarmTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="超出围栏米数" prop="distance"> |
|||
<el-input-number v-model="formData.distance" /> |
|||
</el-form-item> |
|||
<el-form-item label="最远超出米数" prop="maxDistance"> |
|||
<el-input-number v-model="formData.maxDistance" /> |
|||
</el-form-item> |
|||
<el-form-item label="开始时间" prop="tAlarmStart"> |
|||
<el-date-picker |
|||
v-model="formData.tAlarmStart" |
|||
type="date" |
|||
value-format="x" |
|||
placeholder="选择开始时间" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="结束时间" prop="tAlarmEnd"> |
|||
<el-date-picker |
|||
v-model="formData.tAlarmEnd" |
|||
type="date" |
|||
value-format="x" |
|||
placeholder="选择结束时间" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-radio-group v-model="formData.status"> |
|||
<el-radio |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_HANDLE_STATUS)" |
|||
:key="item.value" |
|||
:value="item.value" |
|||
> |
|||
{{ item.label }} |
|||
</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { FenceAlarmApi, FenceAlarm } from '@/api/gas/fencealarm' |
|||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
|||
import { HandDetector } from '@/api/gas/handdetector' |
|||
import { Fence } from '@/api/gas/fence' |
|||
import { AlarmType } from '@/api/gas/alarmtype' |
|||
|
|||
/** GAS手持探测器围栏报警 表单 */ |
|||
defineOptions({ name: 'FenceAlarmForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
const props = defineProps({ |
|||
handDetector: { |
|||
type: Array as PropType<HandDetector[]>, |
|||
required: true |
|||
}, |
|||
fences: { |
|||
type: Array as PropType<Fence[]>, |
|||
required: true |
|||
}, |
|||
alarmTypes: { |
|||
type: Array as PropType<AlarmType[]>, |
|||
required: true |
|||
} |
|||
}) |
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const formData = ref({ |
|||
id: undefined, |
|||
detectorId: undefined, |
|||
fenceId: undefined, |
|||
type: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
distance: undefined, |
|||
maxDistance: undefined, |
|||
tAlarmStart: undefined, |
|||
tAlarmEnd: undefined, |
|||
status: undefined, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
detectorId: [{ required: true, message: '持有人不能为空', trigger: 'blur' }], |
|||
fenceId: [{ required: true, message: '围栏不能为空', trigger: 'blur' }], |
|||
type: [{ required: true, message: '报警类型不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await FenceAlarmApi.getFenceAlarm(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as FenceAlarm |
|||
if (formType.value === 'create') { |
|||
await FenceAlarmApi.createFenceAlarm(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await FenceAlarmApi.updateFenceAlarm(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
detectorId: undefined, |
|||
fenceId: undefined, |
|||
type: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
distance: undefined, |
|||
maxDistance: undefined, |
|||
tAlarmStart: undefined, |
|||
tAlarmEnd: undefined, |
|||
status: undefined, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
</script> |
@ -0,0 +1,337 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="120px" |
|||
> |
|||
<el-form-item label="持有人" prop="detectorId"> |
|||
<el-select |
|||
v-model="queryParams.detectorId" |
|||
placeholder="请选择持有人" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getHandDetectorList" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="围栏" prop="fenceId"> |
|||
<el-select |
|||
v-model="queryParams.fenceId" |
|||
placeholder="请选择围栏" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getFences" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="报警类型" prop="type"> |
|||
<el-select |
|||
v-model="queryParams.type" |
|||
placeholder="请选择报警类型" |
|||
clearable |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getAlarmTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="开始时间" prop="tAlarmStart"> |
|||
<el-date-picker |
|||
v-model="queryParams.tAlarmStart" |
|||
value-format="YYYY-MM-DD" |
|||
type="date" |
|||
placeholder="选择开始时间" |
|||
clearable |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="结束时间" prop="tAlarmEnd"> |
|||
<el-date-picker |
|||
v-model="queryParams.tAlarmEnd" |
|||
value-format="YYYY-MM-DD" |
|||
type="date" |
|||
placeholder="选择结束时间" |
|||
clearable |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> |
|||
<el-option |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_HANDLE_STATUS)" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:fence-alarm:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:fence-alarm:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:fence-alarm:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="持有人" align="center" prop="detectorId"> |
|||
<template #default="scope"> |
|||
{{ |
|||
handDetectorStore.getHandDetectorList.find((item) => item.id === scope.row.detectorId) |
|||
?.name |
|||
}} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="围栏" align="center" prop="fenceId"> |
|||
<template #default="scope"> |
|||
{{ handDetectorStore.getFences.find((item) => item.id === scope.row.fenceId)?.name }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="报警类型" align="center" prop="type"> |
|||
<template #default="scope"> |
|||
{{ handDetectorStore.getAlarmTypes.find((item) => item.id === scope.row.type)?.name }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="超出围栏米数" align="center" prop="distance" /> |
|||
<el-table-column label="最远超出米数" align="center" prop="maxDistance" /> |
|||
<el-table-column |
|||
label="开始时间" |
|||
align="center" |
|||
prop="tAlarmStart" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column |
|||
label="结束时间" |
|||
align="center" |
|||
prop="tAlarmEnd" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="状态" align="center" prop="status"> |
|||
<template #default="scope"> |
|||
<DictTag :type="DICT_TYPE.HAND_DETECTOR_HANDLE_STATUS" :value="scope.row.status" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:fence-alarm:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:fence-alarm:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<FenceAlarmForm |
|||
ref="formRef" |
|||
@success="getList" |
|||
:handDetector="handDetectorStore.getHandDetectorList" |
|||
:fences="handDetectorStore.getFences" |
|||
:alarmTypes="handDetectorStore.getAlarmTypes" |
|||
/> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { dateFormatter } from '@/utils/formatTime' |
|||
import download from '@/utils/download' |
|||
import { FenceAlarmApi, FenceAlarm } from '@/api/gas/fencealarm' |
|||
import FenceAlarmForm from './FenceAlarmForm.vue' |
|||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
|||
import { useHandDetectorStore } from '@/store/modules/handDetector' |
|||
|
|||
/** GAS手持探测器围栏报警 列表 */ |
|||
defineOptions({ name: 'FenceAlarm' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
const handDetectorStore = useHandDetectorStore() // 手持探测器 store |
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<FenceAlarm[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
detectorId: undefined, |
|||
fenceId: undefined, |
|||
type: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
distance: undefined, |
|||
maxDistance: undefined, |
|||
tAlarmStart: undefined, |
|||
tAlarmEnd: undefined, |
|||
status: undefined, |
|||
remark: undefined, |
|||
createTime: [] |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await FenceAlarmApi.getFenceAlarmPage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await FenceAlarmApi.deleteFenceAlarm(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS手持探测器围栏报警 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await FenceAlarmApi.deleteFenceAlarmList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: FenceAlarm[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await FenceAlarmApi.exportFenceAlarm(queryParams) |
|||
download.excel(data, 'GAS手持探测器围栏报警.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
handDetectorStore.getAllHandDetector() |
|||
handDetectorStore.getAllFences() |
|||
handDetectorStore.getAllAlarmTypes() |
|||
}) |
|||
</script> |
@ -0,0 +1,114 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="100px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="名称" prop="name"> |
|||
<el-input v-model="formData.name" placeholder="请输入名称" /> |
|||
</el-form-item> |
|||
<el-form-item label="化学式" prop="chemical"> |
|||
<el-input v-model="formData.chemical" placeholder="请输入化学式" /> |
|||
</el-form-item> |
|||
<el-form-item label="单位" prop="unit"> |
|||
<el-input v-model="formData.unit" placeholder="请输入单位" /> |
|||
</el-form-item> |
|||
<el-form-item label="排序" prop="sortOrder"> |
|||
<el-input v-model="formData.sortOrder" placeholder="请输入排序" /> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { TypeApi, Type } from '@/api/gas/gastype' |
|||
|
|||
/** GAS气体 表单 */ |
|||
defineOptions({ name: 'TypeForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
|
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const formData = ref({ |
|||
id: undefined, |
|||
name: undefined, |
|||
chemical: undefined, |
|||
unit: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], |
|||
chemical: [{ required: true, message: '化学式不能为空', trigger: 'blur' }], |
|||
unit: [{ required: true, message: '单位不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await TypeApi.getType(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as Type |
|||
if (formType.value === 'create') { |
|||
await TypeApi.createType(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await TypeApi.updateType(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
name: undefined, |
|||
chemical: undefined, |
|||
unit: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
</script> |
@ -0,0 +1,217 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="68px" |
|||
> |
|||
<el-form-item label="名称" prop="name"> |
|||
<el-input |
|||
v-model="queryParams.name" |
|||
placeholder="请输入名称" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:type:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:type:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:type:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="名称" align="center" prop="name" /> |
|||
<el-table-column label="化学式" align="center" prop="chemical" /> |
|||
<el-table-column label="单位" align="center" prop="unit" /> |
|||
<el-table-column label="排序" align="center" prop="sortOrder" /> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:type:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:type:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<TypeForm ref="formRef" @success="getList" /> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { dateFormatter } from '@/utils/formatTime' |
|||
import download from '@/utils/download' |
|||
import { TypeApi, Type } from '@/api/gas/gastype' |
|||
import TypeForm from './TypeForm.vue' |
|||
|
|||
/** GAS气体 列表 */ |
|||
defineOptions({ name: 'GasType' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
|
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<Type[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
name: undefined, |
|||
chemical: undefined, |
|||
unit: undefined, |
|||
sortOrder: undefined, |
|||
remark: undefined, |
|||
createTime: [] |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await TypeApi.getTypePage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await TypeApi.deleteType(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS气体 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await TypeApi.deleteTypeList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: Type[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await TypeApi.exportType(queryParams) |
|||
download.excel(data, 'GAS气体.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
}) |
|||
</script> |
@ -0,0 +1,249 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="120px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="持有人" prop="detectorId"> |
|||
<el-select |
|||
v-model="formData.detectorId" |
|||
placeholder="请选择持有人" |
|||
@change="handleDetectorIdChange" |
|||
> |
|||
<el-option |
|||
v-for="item in props.handDetector" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="设备编号" prop="sn"> |
|||
<el-input v-model="formData.sn" placeholder="请输入设备编号" :disabled="true" /> |
|||
</el-form-item> |
|||
<el-form-item label="报警类型" prop="alarmType"> |
|||
<el-select |
|||
v-model="formData.alarmType" |
|||
@change="handleAlarmTypeChange" |
|||
placeholder="请选择报警类型" |
|||
> |
|||
<el-option |
|||
v-for="item in props.alarmTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="警报方式/级别" prop="alarmLevel"> |
|||
<el-select v-model="formData.alarmLevel" placeholder="请选择警报方式/级别" :disabled="true"> |
|||
<el-option |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_ALARM_LEVEL)" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="气体类型" prop="gasType"> |
|||
<el-select |
|||
v-model="formData.gasType" |
|||
placeholder="请选择气体类型" |
|||
@change="handleGasTypeChange" |
|||
> |
|||
<el-option |
|||
v-for="item in props.gasTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="单位" prop="unit"> |
|||
<el-input v-model="formData.unit" placeholder="请输入单位" :disabled="true" /> |
|||
</el-form-item> |
|||
<el-form-item label="首报值" prop="vAlarmFirst"> |
|||
<el-input-number v-model="formData.vAlarmFirst" /> |
|||
</el-form-item> |
|||
<el-form-item label="最值" prop="vAlarmMaximum"> |
|||
<el-input-number v-model="formData.vAlarmMaximum" /> |
|||
</el-form-item> |
|||
<el-form-item label="开始时间" prop="tAlarmStart"> |
|||
<el-date-picker |
|||
v-model="formData.tAlarmStart" |
|||
type="date" |
|||
value-format="x" |
|||
placeholder="选择开始时间" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="结束时间" prop="tAlarmEnd"> |
|||
<el-date-picker |
|||
v-model="formData.tAlarmEnd" |
|||
type="date" |
|||
value-format="x" |
|||
placeholder="选择结束时间" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-radio-group v-model="formData.status"> |
|||
<el-radio |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_HANDLE_STATUS)" |
|||
:key="item.value" |
|||
:value="item.value" |
|||
> |
|||
{{ item.label }} |
|||
</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { HandAlarmApi, HandAlarm } from '@/api/gas/handalarm' |
|||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' |
|||
import { HandDetector } from '@/api/gas/handdetector' |
|||
import { AlarmType } from '@/api/gas/alarmtype' |
|||
import { Type } from '@/api/gas/gastype' |
|||
/** GAS手持探测器警报 表单 */ |
|||
defineOptions({ name: 'HandAlarmForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
|
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const props = defineProps({ |
|||
handDetector: { |
|||
type: Array as PropType<HandDetector[]>, |
|||
required: true |
|||
}, |
|||
alarmTypes: { |
|||
type: Array as PropType<AlarmType[]>, |
|||
required: true |
|||
}, |
|||
gasTypes: { |
|||
type: Array as PropType<Type[]>, |
|||
required: true |
|||
} |
|||
}) |
|||
const formData = ref({ |
|||
id: undefined, |
|||
detectorId: undefined, |
|||
sn: '', |
|||
alarmType: undefined, |
|||
alarmLevel: 0, |
|||
gasType: '', |
|||
unit: '', |
|||
location: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
vAlarmFirst: undefined, |
|||
vAlarmMaximum: undefined, |
|||
tAlarmStart: undefined, |
|||
tAlarmEnd: undefined, |
|||
status: 0, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
detectorId: [{ required: true, message: '持有人不能为空', trigger: 'change' }], |
|||
alarmType: [{ required: true, message: '报警类型不能为空', trigger: 'change' }], |
|||
gasType: [{ required: true, message: '气体类型不能为空', trigger: 'change' }], |
|||
unit: [{ required: true, message: '单位不能为空', trigger: 'blur' }], |
|||
remark: [{ required: true, message: '备注不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await HandAlarmApi.getHandAlarm(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as HandAlarm |
|||
if (formType.value === 'create') { |
|||
await HandAlarmApi.createHandAlarm(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await HandAlarmApi.updateHandAlarm(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
detectorId: undefined, |
|||
sn: '', |
|||
alarmType: undefined, |
|||
alarmLevel: 0, |
|||
gasType: '', |
|||
unit: '', |
|||
location: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
vAlarmFirst: undefined, |
|||
vAlarmMaximum: undefined, |
|||
tAlarmStart: undefined, |
|||
tAlarmEnd: undefined, |
|||
status: 0, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
|
|||
/** 气体类型改变 */ |
|||
const handleGasTypeChange = (value: number) => { |
|||
formData.value.unit = props.gasTypes.find((item) => item.id === value)?.unit || '' |
|||
} |
|||
|
|||
/** 手持表id改变 */ |
|||
const handleDetectorIdChange = (value: number) => { |
|||
formData.value.sn = props.handDetector.find((item) => item.id === value)?.sn || '' |
|||
} |
|||
|
|||
/** 报警类型改变 */ |
|||
const handleAlarmTypeChange = (value: number) => { |
|||
formData.value.alarmLevel = props.alarmTypes.find((item) => item.id === value)?.level || 0 |
|||
formData.value.gasType = props.gasTypes.find((item) => item.id === value)?.name || '' |
|||
formData.value.unit = props.gasTypes.find((item) => item.id === value)?.unit || '' |
|||
} |
|||
</script> |
@ -0,0 +1,333 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="120px" |
|||
> |
|||
<el-form-item label="持有人" prop="detectorId"> |
|||
<el-select |
|||
v-model="queryParams.detectorId" |
|||
placeholder="请选择持有人" |
|||
clearable |
|||
filterable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getHandDetectorList" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="设备编号" prop="sn"> |
|||
<el-input |
|||
v-model="queryParams.sn" |
|||
placeholder="请输入设备编号" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="报警类型" prop="alarmType"> |
|||
<el-select |
|||
v-model="queryParams.alarmType" |
|||
placeholder="请选择报警类型" |
|||
clearable |
|||
class="!w-240px" |
|||
> |
|||
<el-option |
|||
v-for="item in handDetectorStore.getAlarmTypes" |
|||
:key="item.id" |
|||
:label="item.name" |
|||
:value="item.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="开始时间" prop="tAlarmStart"> |
|||
<el-date-picker |
|||
v-model="queryParams.tAlarmStart" |
|||
value-format="YYYY-MM-DD" |
|||
type="date" |
|||
placeholder="选择开始时间" |
|||
clearable |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="结束时间" prop="tAlarmEnd"> |
|||
<el-date-picker |
|||
v-model="queryParams.tAlarmEnd" |
|||
value-format="YYYY-MM-DD" |
|||
type="date" |
|||
placeholder="选择结束时间" |
|||
clearable |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> |
|||
<el-option |
|||
v-for="item in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_HANDLE_STATUS)" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:hand-alarm:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:hand-alarm:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:hand-alarm:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="持有人" align="center" prop="detectorId"> |
|||
<template #default="scope"> |
|||
{{ |
|||
handDetectorStore.getHandDetectorList.find((item) => item.id === scope.row.detectorId) |
|||
?.name |
|||
}} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="设备编号" align="center" prop="sn" /> |
|||
<el-table-column label="报警类型" align="center" prop="alarmType"> |
|||
<template #default="scope"> |
|||
{{ |
|||
handDetectorStore.getAlarmTypes.find((item) => item.id === scope.row.alarmType)?.name |
|||
}} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="气体类型" align="center" prop="gasType"> |
|||
<template #default="scope"> |
|||
{{ handDetectorStore.getGasTypes.find((item) => item.id === scope.row.gasType)?.name }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="首报值" align="center" prop="vAlarmFirst" /> |
|||
<el-table-column label="最值" align="center" prop="vAlarmMaximum" /> |
|||
<el-table-column |
|||
label="开始时间" |
|||
align="center" |
|||
prop="tAlarmStart" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column |
|||
label="结束时间" |
|||
align="center" |
|||
prop="tAlarmEnd" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="状态" align="center" prop="status" /> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
:formatter="dateFormatter" |
|||
width="180px" |
|||
/> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:hand-alarm:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:hand-alarm:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<HandAlarmForm |
|||
ref="formRef" |
|||
@success="getList" |
|||
:handDetector="handDetectorStore.getHandDetectorList" |
|||
:gasTypes="handDetectorStore.getGasTypes" |
|||
:alarmTypes="handDetectorStore.getAlarmTypes" |
|||
/> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import { dateFormatter } from '@/utils/formatTime' |
|||
import download from '@/utils/download' |
|||
import { HandAlarmApi, HandAlarm } from '@/api/gas/handalarm' |
|||
import HandAlarmForm from './HandAlarmForm.vue' |
|||
import { useHandDetectorStore } from '@/store/modules/handDetector' |
|||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
|||
/** 手持探测器警报 列表 */ |
|||
defineOptions({ name: 'HandAlarm' }) |
|||
|
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
const handDetectorStore = useHandDetectorStore() |
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<HandAlarm[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
detectorId: undefined, |
|||
sn: undefined, |
|||
alarmType: undefined, |
|||
alarmLevel: undefined, |
|||
gasType: undefined, |
|||
unit: undefined, |
|||
location: undefined, |
|||
picX: undefined, |
|||
picY: undefined, |
|||
vAlarmFirst: undefined, |
|||
vAlarmMaximum: undefined, |
|||
tAlarmStart: undefined, |
|||
tAlarmEnd: undefined, |
|||
status: undefined, |
|||
remark: undefined, |
|||
createTime: [] |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
|
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await HandAlarmApi.getHandAlarmPage(queryParams) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await HandAlarmApi.deleteHandAlarm(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS手持探测器警报 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await HandAlarmApi.deleteHandAlarmList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: HandAlarm[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await HandAlarmApi.exportHandAlarm(queryParams) |
|||
download.excel(data, '手持探测器警报.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
handDetectorStore.getAllHandDetector() |
|||
handDetectorStore.getAllAlarmTypes() |
|||
handDetectorStore.getAllGasTypes() |
|||
}) |
|||
</script> |
@ -0,0 +1,217 @@ |
|||
<template> |
|||
<Dialog :title="dialogTitle" v-model="dialogVisible"> |
|||
<el-form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="formRules" |
|||
label-width="100px" |
|||
v-loading="formLoading" |
|||
> |
|||
<el-form-item label="SN" prop="sn"> |
|||
<el-input v-model="formData.sn" placeholder="请输入SN" /> |
|||
</el-form-item> |
|||
<el-form-item label="持有人" prop="name"> |
|||
<el-input v-model="formData.name" placeholder="请输入持有人" /> |
|||
</el-form-item> |
|||
<el-form-item label="应用围栏" prop="fenceIdsArray"> |
|||
<el-select v-model="formData.fenceIdsArray" placeholder="请选择应用围栏" multiple> |
|||
<el-option |
|||
v-for="fence in fences" |
|||
:key="fence.id" |
|||
:label="fence.name" |
|||
:value="fence.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="气体类型" prop="gasTypeId"> |
|||
<el-select |
|||
v-model="formData.gasTypeId" |
|||
placeholder="请选择气体类型" |
|||
@change="handleGasTypeChange" |
|||
> |
|||
<el-option |
|||
v-for="gasType in gasTypes" |
|||
:key="gasType.id" |
|||
:label="gasType.name" |
|||
:value="gasType.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="气体化学式" prop="gasChemical"> |
|||
<el-input v-model="formData.gasChemical" placeholder="请输入气体化学式" :disabled="true" /> |
|||
</el-form-item> |
|||
<el-form-item label="单位" prop="unit"> |
|||
<el-input v-model="formData.unit" placeholder="请输入单位" /> |
|||
</el-form-item> |
|||
<el-form-item label="最小值" prop="min"> |
|||
<el-input v-model="formData.min" placeholder="请输入最小值" /> |
|||
</el-form-item> |
|||
<el-form-item label="最大值" prop="max"> |
|||
<el-input v-model="formData.max" placeholder="请输入最大值" /> |
|||
</el-form-item> |
|||
<el-form-item label="设备型号" prop="model"> |
|||
<el-input v-model="formData.model" placeholder="请输入设备型号" /> |
|||
</el-form-item> |
|||
<el-form-item label="生产厂家" prop="manufacturer"> |
|||
<el-input v-model="formData.manufacturer" placeholder="请输入生产厂家" /> |
|||
</el-form-item> |
|||
<el-form-item label="低电量报警" prop="batteryAlarmValue"> |
|||
<el-input-number |
|||
:controls="false" |
|||
style="width: 100%" |
|||
v-model="formData.batteryAlarmValue" |
|||
placeholder="请输入低电量报警报警值" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="启用状态" prop="enableStatus"> |
|||
<el-radio-group v-model="formData.enableStatus"> |
|||
<el-radio-button |
|||
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_ENABLE_STATUS)" |
|||
:key="dict.value" |
|||
:value="dict.value" |
|||
> |
|||
{{ dict.label }} |
|||
</el-radio-button> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="数值除数" prop="accuracy"> |
|||
<el-input-number v-model="formData.accuracy" placeholder="请输入数值除数" /> |
|||
</el-form-item> |
|||
<el-form-item label="排序" prop="sortOrder"> |
|||
<el-input v-model="formData.sortOrder" placeholder="请输入排序" /> |
|||
</el-form-item> |
|||
<el-form-item label="备注" prop="remark"> |
|||
<el-input v-model="formData.remark" placeholder="请输入备注" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> |
|||
<el-button @click="dialogVisible = false">取 消</el-button> |
|||
</template> |
|||
</Dialog> |
|||
</template> |
|||
<script setup lang="ts"> |
|||
import { HandDetectorApi, HandDetector } from '@/api/gas/handdetector' |
|||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' |
|||
import { Fence } from '@/api/gas/fence' |
|||
import { Type } from '@/api/gas/gastype' |
|||
/** GAS手持探测器 表单 */ |
|||
defineOptions({ name: 'HandDetectorForm' }) |
|||
|
|||
const { t } = useI18n() // 国际化 |
|||
const message = useMessage() // 消息弹窗 |
|||
|
|||
const dialogVisible = ref(false) // 弹窗的是否展示 |
|||
const dialogTitle = ref('') // 弹窗的标题 |
|||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
|||
const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
|||
const props = defineProps({ |
|||
fences: { |
|||
type: Array as PropType<Fence[]>, |
|||
required: true |
|||
}, |
|||
gasTypes: { |
|||
type: Array as PropType<Type[]>, |
|||
required: true |
|||
} |
|||
}) |
|||
const formData = ref({ |
|||
id: undefined, |
|||
sn: undefined, |
|||
name: undefined, |
|||
fenceIds: '', |
|||
fenceIdsArray: [], |
|||
gasTypeId: undefined, |
|||
gasChemical: '', |
|||
min: 0, |
|||
max: undefined, |
|||
unit: '', |
|||
model: undefined, |
|||
manufacturer: undefined, |
|||
batteryAlarmValue: undefined, |
|||
enableStatus: 1, |
|||
accuracy: 1, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
}) |
|||
const formRules = reactive({ |
|||
sn: [{ required: true, message: 'SN不能为空', trigger: 'blur' }], |
|||
name: [{ required: true, message: '持有人不能为空', trigger: 'blur' }], |
|||
gasTypeId: [{ required: true, message: '气体类型不能为空', trigger: 'blur' }], |
|||
enableStatus: [{ required: true, message: '启用状态不能为空', trigger: 'blur' }] |
|||
}) |
|||
const formRef = ref() // 表单 Ref |
|||
|
|||
/** 打开弹窗 */ |
|||
const open = async (type: string, id?: number) => { |
|||
dialogVisible.value = true |
|||
dialogTitle.value = t('action.' + type) |
|||
formType.value = type |
|||
resetForm() |
|||
// 修改时,设置数据 |
|||
if (id) { |
|||
formLoading.value = true |
|||
try { |
|||
formData.value = await HandDetectorApi.getHandDetector(id) |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
|||
|
|||
/** 提交表单 */ |
|||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
|||
const submitForm = async () => { |
|||
// 校验表单 |
|||
await formRef.value.validate() |
|||
// 提交请求 |
|||
formLoading.value = true |
|||
try { |
|||
const data = formData.value as unknown as HandDetector |
|||
data.fenceIds = data.fenceIdsArray?.join(',') || '' |
|||
if (formType.value === 'create') { |
|||
await HandDetectorApi.createHandDetector(data) |
|||
message.success(t('common.createSuccess')) |
|||
} else { |
|||
await HandDetectorApi.updateHandDetector(data) |
|||
message.success(t('common.updateSuccess')) |
|||
} |
|||
dialogVisible.value = false |
|||
// 发送操作成功的事件 |
|||
emit('success') |
|||
} finally { |
|||
formLoading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 重置表单 */ |
|||
const resetForm = () => { |
|||
formData.value = { |
|||
id: undefined, |
|||
sn: undefined, |
|||
name: undefined, |
|||
fenceIds: '', |
|||
fenceIdsArray: [], |
|||
gasTypeId: undefined, |
|||
gasChemical: '', |
|||
min: 0, |
|||
max: undefined, |
|||
unit: '', |
|||
model: undefined, |
|||
manufacturer: undefined, |
|||
batteryAlarmValue: undefined, |
|||
enableStatus: 1, |
|||
accuracy: 1, |
|||
sortOrder: undefined, |
|||
remark: undefined |
|||
} |
|||
formRef.value?.resetFields() |
|||
} |
|||
|
|||
const handleGasTypeChange = (value: number) => { |
|||
formData.value.gasChemical = |
|||
props.gasTypes.find((gasType) => gasType.id === value)?.chemical || '' |
|||
formData.value.unit = props.gasTypes.find((gasType) => gasType.id === value)?.unit || '' |
|||
} |
|||
</script> |
@ -0,0 +1,249 @@ |
|||
<template> |
|||
<ContentWrap> |
|||
<!-- 搜索工作栏 --> |
|||
<el-form |
|||
class="-mb-15px" |
|||
:model="queryParams" |
|||
ref="queryFormRef" |
|||
:inline="true" |
|||
label-width="120px" |
|||
> |
|||
<el-form-item label="SN" prop="sn"> |
|||
<el-input |
|||
v-model="queryParams.sn" |
|||
placeholder="请输入SN" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="持有人" prop="name"> |
|||
<el-input |
|||
v-model="queryParams.name" |
|||
placeholder="请输入持有人" |
|||
clearable |
|||
@keyup.enter="handleQuery" |
|||
class="!w-240px" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
|||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
@click="openForm('create')" |
|||
v-hasPermi="['gas:hand-detector:create']" |
|||
> |
|||
<Icon icon="ep:plus" class="mr-5px" /> 新增 |
|||
</el-button> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
@click="handleExport" |
|||
:loading="exportLoading" |
|||
v-hasPermi="['gas:hand-detector:export']" |
|||
> |
|||
<Icon icon="ep:download" class="mr-5px" /> 导出 |
|||
</el-button> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
:disabled="isEmpty(checkedIds)" |
|||
@click="handleDeleteBatch" |
|||
v-hasPermi="['gas:hand-detector:delete']" |
|||
> |
|||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</ContentWrap> |
|||
|
|||
<!-- 列表 --> |
|||
<ContentWrap> |
|||
<el-table |
|||
row-key="id" |
|||
v-loading="loading" |
|||
:data="list" |
|||
:stripe="true" |
|||
:show-overflow-tooltip="true" |
|||
@selection-change="handleRowCheckboxChange" |
|||
> |
|||
<el-table-column type="selection" width="55" /> |
|||
<el-table-column label="SN" align="center" prop="sn" /> |
|||
<el-table-column label="持有人" align="center" prop="name" /> |
|||
<el-table-column label="应用围栏" align="center" prop="fenceIds"> |
|||
<template #default="scope"> |
|||
{{ |
|||
scope.row.fenceIdsArray && |
|||
scope.row.fenceIdsArray.length > 0 && |
|||
scope.row.fenceIdsArray |
|||
.map((item) => handDetectorStore.getFences.find((fence) => fence.id === item)?.name) |
|||
.join(',') |
|||
}} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="气体类型" align="center" prop="gasTypeId"> |
|||
<template #default="scope"> |
|||
{{ handDetectorStore.getGasTypes.find((item) => item.id === scope.row.gasTypeId)?.name }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="启用状态" align="center" prop="enableStatus"> |
|||
<template #default="scope"> |
|||
<DictTag :type="DICT_TYPE.HAND_DETECTOR_ENABLE_STATUS" :value="scope.row.enableStatus" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="备注" align="center" prop="remark" /> |
|||
<el-table-column label="操作" align="center" min-width="120px"> |
|||
<template #default="scope"> |
|||
<el-button |
|||
link |
|||
type="primary" |
|||
@click="openForm('update', scope.row.id)" |
|||
v-hasPermi="['gas:hand-detector:update']" |
|||
> |
|||
编辑 |
|||
</el-button> |
|||
<el-button |
|||
link |
|||
type="danger" |
|||
@click="handleDelete(scope.row.id)" |
|||
v-hasPermi="['gas:hand-detector:delete']" |
|||
> |
|||
删除 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
<!-- 分页 --> |
|||
<Pagination |
|||
:total="total" |
|||
v-model:page="queryParams.pageNo" |
|||
v-model:limit="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</ContentWrap> |
|||
|
|||
<!-- 表单弹窗:添加/修改 --> |
|||
<HandDetectorForm ref="formRef" @success="getList" :fences="fences" :gasTypes="gasTypes" /> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { isEmpty } from '@/utils/is' |
|||
import download from '@/utils/download' |
|||
import { HandDetectorApi, HandDetector } from '@/api/gas/handdetector' |
|||
import HandDetectorForm from './HandDetectorForm.vue' |
|||
import { DICT_TYPE } from '@/utils/dict' |
|||
import { Fence } from '@/api/gas/fence' |
|||
import { Type } from '@/api/gas/gastype' |
|||
import { useHandDetectorStore } from '@/store/modules/handDetector' |
|||
/** GAS手持探测器 列表 */ |
|||
defineOptions({ name: 'HandDetector' }) |
|||
const handDetectorStore = useHandDetectorStore() |
|||
const message = useMessage() // 消息弹窗 |
|||
const { t } = useI18n() // 国际化 |
|||
|
|||
const loading = ref(true) // 列表的加载中 |
|||
const list = ref<HandDetector[]>([]) // 列表的数据 |
|||
const total = ref(0) // 列表的总页数 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
sn: undefined, |
|||
name: undefined, |
|||
createTime: [] |
|||
}) |
|||
const queryFormRef = ref() // 搜索的表单 |
|||
const exportLoading = ref(false) // 导出的加载中 |
|||
const fences = ref<Fence[]>([]) |
|||
const gasTypes = ref<Type[]>([]) |
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
loading.value = true |
|||
try { |
|||
const data = await HandDetectorApi.getHandDetectorPage(queryParams) |
|||
data.list.forEach((item: HandDetector) => { |
|||
item.fenceIds && (item.fenceIdsArray = item.fenceIds.split(',')) |
|||
}) |
|||
list.value = data.list |
|||
total.value = data.total |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
/** 搜索按钮操作 */ |
|||
const handleQuery = () => { |
|||
queryParams.pageNo = 1 |
|||
getList() |
|||
} |
|||
|
|||
/** 重置按钮操作 */ |
|||
const resetQuery = () => { |
|||
queryFormRef.value.resetFields() |
|||
handleQuery() |
|||
} |
|||
|
|||
/** 添加/修改操作 */ |
|||
const formRef = ref() |
|||
const openForm = (type: string, id?: number) => { |
|||
formRef.value.open(type, id) |
|||
} |
|||
|
|||
/** 删除按钮操作 */ |
|||
const handleDelete = async (id: number) => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
// 发起删除 |
|||
await HandDetectorApi.deleteHandDetector(id) |
|||
message.success(t('common.delSuccess')) |
|||
// 刷新列表 |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
/** 批量删除GAS手持探测器 */ |
|||
const handleDeleteBatch = async () => { |
|||
try { |
|||
// 删除的二次确认 |
|||
await message.delConfirm() |
|||
await HandDetectorApi.deleteHandDetectorList(checkedIds.value) |
|||
message.success(t('common.delSuccess')) |
|||
await getList() |
|||
} catch {} |
|||
} |
|||
|
|||
const checkedIds = ref<number[]>([]) |
|||
const handleRowCheckboxChange = (records: HandDetector[]) => { |
|||
checkedIds.value = records.map((item) => item.id) |
|||
} |
|||
|
|||
/** 导出按钮操作 */ |
|||
const handleExport = async () => { |
|||
try { |
|||
// 导出的二次确认 |
|||
await message.exportConfirm() |
|||
// 发起导出 |
|||
exportLoading.value = true |
|||
const data = await HandDetectorApi.exportHandDetector(queryParams) |
|||
download.excel(data, '手持探测器.xls') |
|||
} catch { |
|||
} finally { |
|||
exportLoading.value = false |
|||
} |
|||
} |
|||
const getAllFences = async () => { |
|||
fences.value = await handDetectorStore.getAllFences() |
|||
} |
|||
|
|||
const getAllGasTypes = async () => { |
|||
gasTypes.value = await handDetectorStore.getAllGasTypes() |
|||
} |
|||
/** 初始化 **/ |
|||
onMounted(() => { |
|||
getList() |
|||
getAllFences() |
|||
getAllGasTypes() |
|||
}) |
|||
</script> |
Loading…
Reference in new issue