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> |
<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> |
</template> |
||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||
import { useUserStore } from '@/store/modules/user' |
|
||||
|
import HandDeviceHome from '../HandDevice/Home/index.vue' |
||||
defineOptions({ name: 'Index' }) |
defineOptions({ name: 'Index' }) |
||||
const { t } = useI18n() |
|
||||
const userStore = useUserStore() |
|
||||
const loading = ref(false) |
|
||||
const avatar = userStore.getUser.avatar |
|
||||
const username = userStore.getUser.nickname |
|
||||
</script> |
</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