7 changed files with 4279 additions and 0 deletions
@ -0,0 +1,115 @@ |
|||
import request from '@/utils/request' |
|||
|
|||
export function listFence(query) { |
|||
return request({ |
|||
url: '/fence/device/fence/list', |
|||
method: 'get', |
|||
params: query |
|||
}) |
|||
} |
|||
|
|||
export function getFence(id) { |
|||
return request({ |
|||
url: '/fence/device/fence/' + id, |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
export function addFence(data) { |
|||
return request({ |
|||
url: '/fence/device/fence', |
|||
method: 'post', |
|||
data |
|||
}) |
|||
} |
|||
|
|||
export function updateFence(data) { |
|||
return request({ |
|||
url: '/fence/device/fence', |
|||
method: 'put', |
|||
data |
|||
}) |
|||
} |
|||
|
|||
export function removeFence(ids) { |
|||
return request({ |
|||
url: '/fence/device/fence/' + ids, |
|||
method: 'delete' |
|||
}) |
|||
} |
|||
|
|||
export function changeFenceStatus(id, status) { |
|||
return request({ |
|||
url: '/fence/device/fence/' + id + '/status/' + status, |
|||
method: 'put' |
|||
}) |
|||
} |
|||
|
|||
export function listFenceDeviceOptions(query) { |
|||
return request({ |
|||
url: '/fence/device/device/options', |
|||
method: 'get', |
|||
params: query |
|||
}) |
|||
} |
|||
|
|||
export function getFenceMonitorOverview() { |
|||
return request({ |
|||
url: '/fence/device/monitor/overview', |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
export function listFenceMonitorFences(query) { |
|||
return request({ |
|||
url: '/fence/device/monitor/fences', |
|||
method: 'get', |
|||
params: query |
|||
}) |
|||
} |
|||
|
|||
export function listFenceMonitorDevices(query) { |
|||
return request({ |
|||
url: '/fence/device/monitor/devices', |
|||
method: 'get', |
|||
params: query |
|||
}) |
|||
} |
|||
|
|||
export function setFenceMonitorStateNormal(data) { |
|||
return request({ |
|||
url: '/fence/device/monitor/state/normal', |
|||
method: 'put', |
|||
data |
|||
}) |
|||
} |
|||
|
|||
export function listFenceAlarms(query) { |
|||
return request({ |
|||
url: '/fence/device/alarm/list', |
|||
method: 'get', |
|||
params: query |
|||
}) |
|||
} |
|||
export function getTodayAlarmDeviceCount() { |
|||
return request({ |
|||
url: '/fence/device/alarm/today/device-count', |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
export function handleFenceAlarm(id, data) { |
|||
return request({ |
|||
url: '/fence/device/alarm/' + id + '/handle', |
|||
method: 'put', |
|||
data |
|||
}) |
|||
} |
|||
|
|||
export function executeFenceScan() { |
|||
return request({ |
|||
url: '/fence/device/scan/execute', |
|||
method: 'post' |
|||
}) |
|||
} |
|||
|
|||
@ -0,0 +1,257 @@ |
|||
const fenceDrawZh = { |
|||
toolbar: { |
|||
actions: { title: '取消绘制', text: '取消' }, |
|||
finish: { title: '完成绘制', text: '完成' }, |
|||
undo: { title: '删除最后一个点', text: '撤销' }, |
|||
buttons: { polygon: '绘制围栏(多边形)', rectangle: '绘制围栏(矩形)', circle: '绘制围栏(圆形)' } |
|||
}, |
|||
handlers: { |
|||
circle: { start: '点击并拖拽,绘制圆形围栏' }, |
|||
rectangle: { start: '点击并拖拽,绘制矩形围栏' }, |
|||
simpleShape: { end: '释放鼠标完成绘制' }, |
|||
polygon: { start: '点击地图开始绘制围栏', cont: '点击继续绘制围栏', end: '点击起点完成绘制' } |
|||
}, |
|||
edit: { |
|||
actions: { saveTitle: '保存修改', saveText: '保存', cancelTitle: '取消编辑', cancelText: '取消' }, |
|||
buttons: { edit: '编辑围栏', editDisabled: '没有可编辑的围栏', remove: '删除围栏', removeDisabled: '没有可删除的围栏' }, |
|||
handlers: { editText: '拖拽控制点调整围栏', editSubtext: '点击取消可放弃修改', removeText: '点击围栏进行删除' } |
|||
} |
|||
} |
|||
|
|||
const fenceDrawEn = { |
|||
toolbar: { |
|||
actions: { title: 'Cancel drawing', text: 'Cancel' }, |
|||
finish: { title: 'Finish drawing', text: 'Finish' }, |
|||
undo: { title: 'Delete last point', text: 'Undo' }, |
|||
buttons: { polygon: 'Draw polygon fence', rectangle: 'Draw rectangle fence', circle: 'Draw circular fence' } |
|||
}, |
|||
handlers: { |
|||
circle: { start: 'Click and drag to draw a circular fence' }, |
|||
rectangle: { start: 'Click and drag to draw a rectangular fence' }, |
|||
simpleShape: { end: 'Release mouse to finish drawing' }, |
|||
polygon: { start: 'Click map to start drawing fence', cont: 'Click to continue drawing', end: 'Click first point to finish' } |
|||
}, |
|||
edit: { |
|||
actions: { saveTitle: 'Save changes', saveText: 'Save', cancelTitle: 'Cancel editing', cancelText: 'Cancel' }, |
|||
buttons: { edit: 'Edit fence', editDisabled: 'No editable fence', remove: 'Delete fence', removeDisabled: 'No removable fence' }, |
|||
handlers: { editText: 'Drag handles to adjust fence', editSubtext: 'Click cancel to discard changes', removeText: 'Click a fence to remove' } |
|||
} |
|||
} |
|||
|
|||
const fenceDrawFr = { |
|||
toolbar: { |
|||
actions: { title: 'Annuler le tracé', text: 'Annuler' }, |
|||
finish: { title: 'Terminer le tracé', text: 'Terminer' }, |
|||
undo: { title: 'Supprimer le dernier point', text: 'Annuler' }, |
|||
buttons: { polygon: 'Tracer une clôture polygonale', rectangle: 'Tracer une clôture rectangulaire', circle: 'Tracer une clôture circulaire' } |
|||
}, |
|||
handlers: { |
|||
circle: { start: 'Cliquez et faites glisser pour tracer une clôture circulaire' }, |
|||
rectangle: { start: 'Cliquez et faites glisser pour tracer une clôture rectangulaire' }, |
|||
simpleShape: { end: 'Relâchez la souris pour terminer le tracé' }, |
|||
polygon: { start: 'Cliquez sur la carte pour commencer le tracé', cont: 'Cliquez pour continuer le tracé', end: 'Cliquez sur le point de départ pour terminer' } |
|||
}, |
|||
edit: { |
|||
actions: { saveTitle: 'Enregistrer les modifications', saveText: 'Enregistrer', cancelTitle: 'Annuler la modification', cancelText: 'Annuler' }, |
|||
buttons: { edit: 'Modifier la clôture', editDisabled: 'Aucune clôture modifiable', remove: 'Supprimer la clôture', removeDisabled: 'Aucune clôture à supprimer' }, |
|||
handlers: { editText: 'Faites glisser les poignées pour ajuster la clôture', editSubtext: 'Cliquez sur annuler pour ignorer les modifications', removeText: 'Cliquez sur une clôture pour la supprimer' } |
|||
} |
|||
} |
|||
|
|||
const fenceDrawEs = { |
|||
toolbar: { |
|||
actions: { title: 'Cancelar dibujo', text: 'Cancelar' }, |
|||
finish: { title: 'Finalizar dibujo', text: 'Finalizar' }, |
|||
undo: { title: 'Eliminar el último punto', text: 'Deshacer' }, |
|||
buttons: { polygon: 'Dibujar cerca poligonal', rectangle: 'Dibujar cerca rectangular', circle: 'Dibujar cerca circular' } |
|||
}, |
|||
handlers: { |
|||
circle: { start: 'Haz clic y arrastra para dibujar una cerca circular' }, |
|||
rectangle: { start: 'Haz clic y arrastra para dibujar una cerca rectangular' }, |
|||
simpleShape: { end: 'Suelta el ratón para terminar el dibujo' }, |
|||
polygon: { start: 'Haz clic en el mapa para comenzar', cont: 'Haz clic para continuar', end: 'Haz clic en el primer punto para finalizar' } |
|||
}, |
|||
edit: { |
|||
actions: { saveTitle: 'Guardar cambios', saveText: 'Guardar', cancelTitle: 'Cancelar edición', cancelText: 'Cancelar' }, |
|||
buttons: { edit: 'Editar cerca', editDisabled: 'No hay cercas editables', remove: 'Eliminar cerca', removeDisabled: 'No hay cercas para eliminar' }, |
|||
handlers: { editText: 'Arrastra los puntos de control para ajustar la cerca', editSubtext: 'Haz clic en cancelar para descartar cambios', removeText: 'Haz clic en una cerca para eliminarla' } |
|||
} |
|||
} |
|||
|
|||
const fenceDrawPt = { |
|||
toolbar: { |
|||
actions: { title: 'Cancelar desenho', text: 'Cancelar' }, |
|||
finish: { title: 'Finalizar desenho', text: 'Finalizar' }, |
|||
undo: { title: 'Remover o último ponto', text: 'Desfazer' }, |
|||
buttons: { polygon: 'Desenhar cerca poligonal', rectangle: 'Desenhar cerca retangular', circle: 'Desenhar cerca circular' } |
|||
}, |
|||
handlers: { |
|||
circle: { start: 'Clique e arraste para desenhar uma cerca circular' }, |
|||
rectangle: { start: 'Clique e arraste para desenhar uma cerca retangular' }, |
|||
simpleShape: { end: 'Solte o mouse para concluir o desenho' }, |
|||
polygon: { start: 'Clique no mapa para começar', cont: 'Clique para continuar', end: 'Clique no primeiro ponto para finalizar' } |
|||
}, |
|||
edit: { |
|||
actions: { saveTitle: 'Salvar alterações', saveText: 'Salvar', cancelTitle: 'Cancelar edição', cancelText: 'Cancelar' }, |
|||
buttons: { edit: 'Editar cerca', editDisabled: 'Nenhuma cerca editável', remove: 'Excluir cerca', removeDisabled: 'Nenhuma cerca para excluir' }, |
|||
handlers: { editText: 'Arraste os pontos de controle para ajustar a cerca', editSubtext: 'Clique em cancelar para descartar alterações', removeText: 'Clique em uma cerca para removê-la' } |
|||
} |
|||
} |
|||
|
|||
const fenceDrawRu = { |
|||
toolbar: { |
|||
actions: { title: 'Отменить рисование', text: 'Отмена' }, |
|||
finish: { title: 'Завершить рисование', text: 'Готово' }, |
|||
undo: { title: 'Удалить последнюю точку', text: 'Отменить' }, |
|||
buttons: { polygon: 'Нарисовать полигон', rectangle: 'Нарисовать прямоугольник', circle: 'Нарисовать круг' } |
|||
}, |
|||
handlers: { |
|||
circle: { start: 'Нажмите и перетащите, чтобы нарисовать круг' }, |
|||
rectangle: { start: 'Нажмите и перетащите, чтобы нарисовать прямоугольник' }, |
|||
simpleShape: { end: 'Отпустите мышь, чтобы завершить рисование' }, |
|||
polygon: { start: 'Нажмите на карту, чтобы начать рисование', cont: 'Нажмите, чтобы продолжить', end: 'Нажмите на первую точку, чтобы завершить' } |
|||
}, |
|||
edit: { |
|||
actions: { saveTitle: 'Сохранить изменения', saveText: 'Сохранить', cancelTitle: 'Отменить редактирование', cancelText: 'Отмена' }, |
|||
buttons: { edit: 'Редактировать геозону', editDisabled: 'Нет геозон для редактирования', remove: 'Удалить геозону', removeDisabled: 'Нет геозон для удаления' }, |
|||
handlers: { editText: 'Перетаскивайте точки, чтобы изменить геозону', editSubtext: 'Нажмите отмена, чтобы отклонить изменения', removeText: 'Нажмите на геозону, чтобы удалить' } |
|||
} |
|||
} |
|||
const fenceZh = { |
|||
title: '围栏设备管理', |
|||
description: '围栏、设备状态和告警联动放在同一工作台里,支持地图监控、地图绘制和告警处理。', |
|||
tabs: { monitor: '地图监控', fence: '围栏管理', alarm: '告警中心' }, |
|||
sidebar: { |
|||
title: '围栏设备', |
|||
searchPlaceholder: '搜索 SN / 名称 / 地址', |
|||
fenceSearchPlaceholder: '选择围栏名称', |
|||
statusPlaceholder: '全部状态', |
|||
statusAll: '全部状态', |
|||
statusAlert: '告警', |
|||
statusBoundary: '边界', |
|||
statusNormal: '正常', |
|||
unbound: '未关联围栏' |
|||
}, |
|||
device: { normal: '正常', boundary: '边界', outside: '围栏外', alert: '告警' }, |
|||
overview: { online: '监控设备', normalDevice: '正常设备', alertDevice: '告警设备', activeFence: '启用围栏', openAlarm: '未处理告警', todayAlarm: '今日告警' }, |
|||
query: { keyword: '关键字', fenceName: '围栏名称', status: '状态', shapeType: '图形类型', alarmType: '告警类型', handleStatus: '处理状态', alarmLevel: '告警级别', alarmTime: '告警时间' }, |
|||
placeholder: { keyword: '请输入围栏名称或备注', fenceName: '请输入围栏名称', monitorKeyword: '请输入设备 SN / 名称 / 地址', alarmKeyword: '请输入告警内容', alarmSn: '请输入设备 SN', alarmFenceName: '请输入围栏名称', alarmDeviceName: '请输入设备名称', alarmTimeStart: '开始时间', alarmTimeEnd: '结束时间' }, |
|||
button: { add: '新增围栏', edit: '编辑', remove: '删除', scan: '执行扫描', refresh: '刷新', refreshAll: '刷新全局', save: '保存', handle: '处理告警', locate: '定位', more: '更多', preview: '预览', resetEditor: '重置编辑器', selectDevice: '选择设备', markNormal: '设为正常', expandMap: '放大地图', restoreMap: '还原地图' }, |
|||
status: { enabled: '启用', disabled: '停用' }, |
|||
shape: { circle: '圆形', rect: '矩形', polygon: '多边形' }, |
|||
alarmRule: { enter: '进入', exit: '离开', inside: '范围内', outside: '范围外', boundary: '边界', exitRecommended: '离开告警(防丢推荐)', enterExit: '进入+离开', enterOnly: '仅进入' }, |
|||
schedule: { all: '全天', custom: '自定义' }, |
|||
level: { critical: '严重', warning: '警告', info: '提示' }, |
|||
alarmStatus: { open: '未处理', acked: '已确认', closed: '已关闭' }, |
|||
monitor: { realtimeTitle: '实时事件' }, |
|||
map: { monitorHint: '地图显示当前设备、围栏和最新告警位置', providerGoogle: '谷歌地图', providerAmap: '高德地图', providerMaptiler: 'MapTiler' }, |
|||
editor: { help: '在下方地图里使用绘图工具画圆、画矩形、画多边形,图形会同步到当前围栏。', createHint: '新增围栏模式,可直接开始绘制。', editHint: '编辑模式已加载,地图上的图形就是当前围栏。', previewHint: '正在展示围栏:{name}', idleHint: '先绘制围栏,或从下方列表选择已有围栏。', syncShape: '同步图形', clearShape: '清空图形' }, |
|||
draw: fenceDrawZh, |
|||
form: { addTitle: '新增围栏', editTitle: '编辑围栏', name: '围栏名称', shapeType: '几何类型', alarmRules: '告警策略', scheduleType: '生效时间', scheduleStart: '开始时间', scheduleEnd: '结束时间', alarmCountdownMinutes: '告警倒计时(小时)', remark: '围栏备注', centerLat: '中心纬度', centerLng: '中心经度', radiusMeter: '半径(米)', geomWkt: '几何 WKT', shapeDataJson: '图形 JSON', deviceIds: '绑定设备', devicePlaceholder: '请选择需要监控的设备', deviceSearchPlaceholder: '搜索 SN / 设备名称 / 地址', timePlaceholder: 'HH:mm:ss' }, |
|||
dialog: { deviceTitle: '选择绑定设备', deviceHint: '仅显示当前企业下已启用的设备,双击行可快速选择。', selectedCount: '已选择 {count} 台设备', alarmLocateTitle: '设备位置定位' }, |
|||
table: { name: '围栏名称', shape: '图形', rules: '告警策略', status: '状态', actions: '操作', sn: '设备 SN', alias: '设备名称', lat: '纬度', lng: '经度', coordinates: '经纬度', address: '位置地址', locationTime: '更新时间', fenceStatus: '围栏状态', lastAlarm: '最近告警', alarmMessage: '告警内容', alarmTime: '告警时间', alarmType: '告警类型', alarmLevel: '告警级别' }, |
|||
message: { loadFailed: '围栏工作台加载失败', mapConfigLoadFailed: '地图配置加载失败', nameRequired: '请先填写围栏名称', ruleRequired: '请至少选择一项告警规则', circleRequired: '圆形围栏需要中心点和半径', shapeRequired: '请先在地图上绘制围栏图形', shapeSynced: '图形已同步到表单', saveSuccess: '保存成功', removeSuccess: '删除成功', confirmRemove: '确认删除选中的围栏吗?', selectFence: '请先选择围栏', handleSuccess: '告警处理成功', markNormalSuccess: '设备状态已设为正常', markNormalFailed: '当前状态无法设为正常', noMonitorDevice: '当前没有可监控设备', noFenceData: '当前没有围栏数据', noAlarmData: '当前没有告警数据', scanSuccessDetail: '扫描完成,设备 {deviceCount} 个,判定 {evaluatedCount} 条,新增告警 {alarmCount} 条。', alarmLocationMissing: '当前告警缺少经纬度,无法定位。', devicePointLoadFailed: '设备坐标加载失败,已先展示围栏' } |
|||
} |
|||
|
|||
const fenceEn = { |
|||
title: 'Fence Device Management', |
|||
description: 'Manage fences, device states, and alarms in one workspace with map monitoring and map drawing.', |
|||
tabs: { monitor: 'Monitor', fence: 'Fences', alarm: 'Alarms' }, |
|||
sidebar: { |
|||
title: 'Fence Devices', |
|||
searchPlaceholder: 'Search SN / name / address', |
|||
fenceSearchPlaceholder: 'Select Fence Name', |
|||
statusPlaceholder: 'All Statuses', |
|||
statusAll: 'All Statuses', |
|||
statusAlert: 'Alert', |
|||
statusBoundary: 'Boundary', |
|||
statusNormal: 'Normal', |
|||
unbound: 'No Fence Linked' |
|||
}, |
|||
device: { normal: 'Normal', boundary: 'Boundary', outside: 'Outside', alert: 'Alert' }, |
|||
overview: { online: 'Monitored Devices', normalDevice: 'Normal Devices', alertDevice: 'Alert Devices', activeFence: 'Active Fences', openAlarm: 'Open Alarms', todayAlarm: 'Today Alarms' }, |
|||
query: { keyword: 'Keyword', fenceName: 'Fence Name', status: 'Status', shapeType: 'Shape', alarmType: 'Alarm Type', handleStatus: 'Handle Status', alarmLevel: 'Alarm Level', alarmTime: 'Alarm Time' }, |
|||
placeholder: { keyword: 'Enter fence name or remark', fenceName: 'Enter fence name', monitorKeyword: 'Enter device SN / name / address', alarmKeyword: 'Enter alarm content', alarmSn: 'Enter device SN', alarmFenceName: 'Enter fence name', alarmDeviceName: 'Enter device name', alarmTimeStart: 'Start time', alarmTimeEnd: 'End time' }, |
|||
button: { add: 'Add Fence', edit: 'Edit', remove: 'Delete', scan: 'Run Scan', refresh: 'Refresh', refreshAll: 'Refresh All', save: 'Save', handle: 'Handle', locate: 'Locate', more: 'More', preview: 'Preview', resetEditor: 'Reset Editor', selectDevice: 'Select Devices', markNormal: 'Set Normal', expandMap: 'Expand Map', restoreMap: 'Restore Map' }, |
|||
status: { enabled: 'Enabled', disabled: 'Disabled' }, |
|||
shape: { circle: 'Circle', rect: 'Rectangle', polygon: 'Polygon' }, |
|||
alarmRule: { enter: 'Enter', exit: 'Exit', inside: 'Inside', outside: 'Outside', boundary: 'Boundary', exitRecommended: 'Exit Alert (Recommended)', enterExit: 'Enter + Exit', enterOnly: 'Enter Only' }, |
|||
schedule: { all: 'All Day', custom: 'Custom' }, |
|||
level: { critical: 'Critical', warning: 'Warning', info: 'Info' }, |
|||
alarmStatus: { open: 'Open', acked: 'Acknowledged', closed: 'Closed' }, |
|||
monitor: { realtimeTitle: 'Realtime Events' }, |
|||
map: { monitorHint: 'The map shows devices, fences, and the latest alarm location', providerGoogle: 'Google Maps', providerAmap: 'Amap', providerMaptiler: 'MapTiler' }, |
|||
editor: { help: 'Use the drawing tools below to draw circles, rectangles, and polygons for the current fence.', createHint: 'Create mode is ready. Start drawing on the map.', editHint: 'Edit mode loaded. The map shape is the current fence.', previewHint: 'Showing fence: {name}', idleHint: 'Draw a fence first or choose one from the list below.', syncShape: 'Sync Shape', clearShape: 'Clear Shape' }, |
|||
draw: fenceDrawEn, |
|||
form: { addTitle: 'Add Fence', editTitle: 'Edit Fence', name: 'Fence Name', shapeType: 'Geometry Type', alarmRules: 'Alarm Rules', scheduleType: 'Schedule', scheduleStart: 'Start Time', scheduleEnd: 'End Time', alarmCountdownMinutes: 'Alarm Countdown (hour)', remark: 'Fence Remark', centerLat: 'Center Lat', centerLng: 'Center Lng', radiusMeter: 'Radius (m)', geomWkt: 'Geometry WKT', shapeDataJson: 'Shape JSON', deviceIds: 'Bind Devices', devicePlaceholder: 'Select devices', deviceSearchPlaceholder: 'Search SN / device name / address', timePlaceholder: 'HH:mm:ss' }, |
|||
dialog: { deviceTitle: 'Select Devices', deviceHint: 'Only active devices in the current business are shown. Double-click a row to select it quickly.', selectedCount: '{count} devices selected', alarmLocateTitle: 'Device Location' }, |
|||
table: { name: 'Fence Name', shape: 'Shape', rules: 'Rules', status: 'Status', actions: 'Actions', sn: 'Device SN', alias: 'Device Name', lat: 'Latitude', lng: 'Longitude', coordinates: 'Coordinates', address: 'Address', locationTime: 'Update Time', fenceStatus: 'Fence Status', lastAlarm: 'Last Alarm', alarmMessage: 'Alarm Message', alarmTime: 'Alarm Time', alarmType: 'Alarm Type', alarmLevel: 'Alarm Level' }, |
|||
message: { loadFailed: 'Failed to load fence workspace', mapConfigLoadFailed: 'Failed to load map config', nameRequired: 'Fence name is required', ruleRequired: 'Select at least one alarm rule', circleRequired: 'Circle fence needs center and radius', shapeRequired: 'Draw the fence shape on the map first', shapeSynced: 'The shape has been synced to the form', saveSuccess: 'Saved successfully', removeSuccess: 'Deleted successfully', confirmRemove: 'Delete the selected fences?', selectFence: 'Select fences first', handleSuccess: 'Alarm handled successfully', markNormalSuccess: 'State has been set to normal', markNormalFailed: 'Unable to set current state to normal', noMonitorDevice: 'No device available', noFenceData: 'No fence data', noAlarmData: 'No alarm data', scanSuccessDetail: 'Scan finished: {deviceCount} devices, {evaluatedCount} evaluations, {alarmCount} new alarms.', alarmLocationMissing: 'This alarm has no coordinates and cannot be located.', devicePointLoadFailed: 'Failed to load device coordinates, fence is shown first.' } |
|||
} |
|||
|
|||
|
|||
const fenceFr = { |
|||
...fenceEn, |
|||
tabs: { monitor: 'Surveillance', fence: 'Clôtures', alarm: 'Alertes' }, |
|||
editor: { |
|||
...fenceEn.editor, |
|||
help: 'Utilisez les outils ci-dessous pour dessiner des cercles, rectangles et polygones pour la clôture actuelle.', |
|||
createHint: 'Mode création activé. Commencez à dessiner sur la carte.', |
|||
editHint: 'Mode édition chargé. La forme affichée correspond à la clôture en cours.', |
|||
previewHint: 'Clôture affichée : {name}', |
|||
idleHint: 'Dessinez d’abord une clôture ou choisissez-en une dans la liste.' |
|||
}, |
|||
draw: fenceDrawFr |
|||
} |
|||
|
|||
const fenceEs = { |
|||
...fenceEn, |
|||
tabs: { monitor: 'Monitoreo', fence: 'Cercas', alarm: 'Alarmas' }, |
|||
editor: { |
|||
...fenceEn.editor, |
|||
help: 'Usa las herramientas de dibujo para crear círculos, rectángulos y polígonos para la cerca actual.', |
|||
createHint: 'Modo creación listo. Comienza a dibujar en el mapa.', |
|||
editHint: 'Modo edición cargado. La forma del mapa corresponde a la cerca actual.', |
|||
previewHint: 'Cerca mostrada: {name}', |
|||
idleHint: 'Primero dibuja una cerca o elige una de la lista.' |
|||
}, |
|||
draw: fenceDrawEs |
|||
} |
|||
|
|||
const fencePt = { |
|||
...fenceEn, |
|||
tabs: { monitor: 'Monitoramento', fence: 'Cercas', alarm: 'Alertas' }, |
|||
editor: { |
|||
...fenceEn.editor, |
|||
help: 'Use as ferramentas para desenhar círculos, retângulos e polígonos para a cerca atual.', |
|||
createHint: 'Modo de criação pronto. Comece a desenhar no mapa.', |
|||
editHint: 'Modo de edição carregado. A forma no mapa é a cerca atual.', |
|||
previewHint: 'Cerca exibida: {name}', |
|||
idleHint: 'Desenhe uma cerca primeiro ou escolha uma da lista.' |
|||
}, |
|||
draw: fenceDrawPt |
|||
} |
|||
|
|||
const fenceRu = { |
|||
...fenceEn, |
|||
tabs: { monitor: 'Мониторинг', fence: 'Геозоны', alarm: 'Тревоги' }, |
|||
editor: { |
|||
...fenceEn.editor, |
|||
help: 'Используйте инструменты рисования, чтобы создать круг, прямоугольник или полигон для текущей геозоны.', |
|||
createHint: 'Режим создания готов. Начните рисовать на карте.', |
|||
editHint: 'Режим редактирования загружен. Фигура на карте — текущая геозона.', |
|||
previewHint: 'Показана геозона: {name}', |
|||
idleHint: 'Сначала нарисуйте геозону или выберите ее из списка.' |
|||
}, |
|||
draw: fenceDrawRu |
|||
} |
|||
const fenceMessages = { |
|||
'zh-CN': { fenceDevice: fenceZh }, |
|||
'en-US': { fenceDevice: fenceEn }, |
|||
'fr-FR': { fenceDevice: fenceFr }, |
|||
'es-ES': { fenceDevice: fenceEs }, |
|||
'pt-BR': { fenceDevice: fencePt }, |
|||
'ru-RU': { fenceDevice: fenceRu } |
|||
} |
|||
|
|||
export default fenceMessages |
|||
@ -0,0 +1,73 @@ |
|||
import { loadLeaflet } from "@/utils/loadLeaflet"; |
|||
|
|||
let leafletDrawPromise = null; |
|||
|
|||
function ensureLeafletDrawCss() { |
|||
const styleId = "geotag-leaflet-draw-style"; |
|||
if (document.getElementById(styleId)) { |
|||
return; |
|||
} |
|||
const link = document.createElement("link"); |
|||
link.id = styleId; |
|||
link.rel = "stylesheet"; |
|||
link.href = "https://unpkg.com/[email protected]/dist/leaflet.draw.css"; |
|||
document.head.appendChild(link); |
|||
} |
|||
|
|||
export function loadLeafletDraw() { |
|||
if (typeof window === "undefined") { |
|||
return Promise.reject(new Error("Current environment does not support Leaflet.Draw")); |
|||
} |
|||
|
|||
if (window.L && window.L.Control && window.L.Control.Draw) { |
|||
return Promise.resolve(window.L); |
|||
} |
|||
|
|||
if (leafletDrawPromise) { |
|||
return leafletDrawPromise; |
|||
} |
|||
|
|||
leafletDrawPromise = loadLeaflet().then((L) => { |
|||
if (L && L.Control && L.Control.Draw) { |
|||
return L; |
|||
} |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
ensureLeafletDrawCss(); |
|||
|
|||
const scriptId = "geotag-leaflet-draw-script"; |
|||
const existingScript = document.getElementById(scriptId); |
|||
|
|||
const handleResolve = () => { |
|||
if (window.L && window.L.Control && window.L.Control.Draw) { |
|||
resolve(window.L); |
|||
return; |
|||
} |
|||
leafletDrawPromise = null; |
|||
reject(new Error("Leaflet.Draw script failed to load")); |
|||
}; |
|||
|
|||
const handleError = () => { |
|||
leafletDrawPromise = null; |
|||
reject(new Error("Leaflet.Draw script failed to load")); |
|||
}; |
|||
|
|||
if (existingScript) { |
|||
existingScript.addEventListener("load", handleResolve, { once: true }); |
|||
existingScript.addEventListener("error", handleError, { once: true }); |
|||
return; |
|||
} |
|||
|
|||
const script = document.createElement("script"); |
|||
script.id = scriptId; |
|||
script.async = true; |
|||
script.defer = true; |
|||
script.src = "https://unpkg.com/[email protected]/dist/leaflet.draw.js"; |
|||
script.onload = handleResolve; |
|||
script.onerror = handleError; |
|||
document.head.appendChild(script); |
|||
}); |
|||
}); |
|||
|
|||
return leafletDrawPromise; |
|||
} |
|||
@ -0,0 +1,462 @@ |
|||
<template> |
|||
<div class="app-container history-trajectory-page" ref="pageRoot"> |
|||
<div class="history-trajectory-layout" :style="{ height: layoutHeight + 'px' }"> |
|||
<section class="panel left"> |
|||
<div class="panel-title">{{ $t("device.trajectory.page.title.deviceList") }}</div> |
|||
|
|||
<el-form :model="query" class="query-form" @submit.native.prevent> |
|||
<el-form-item class="query-compact-item"> |
|||
<el-input |
|||
v-model="query.sn" |
|||
size="mini" |
|||
clearable |
|||
:placeholder="$t('device.trajectory.page.placeholder.sn')" |
|||
@keyup.enter.native="searchList" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item class="query-compact-item"> |
|||
<el-input |
|||
v-model="query.alias" |
|||
size="mini" |
|||
clearable |
|||
:placeholder="$t('device.trajectory.page.placeholder.alias')" |
|||
@keyup.enter.native="searchList" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item class="query-compact-item"> |
|||
<el-input |
|||
v-model="query.address" |
|||
size="mini" |
|||
clearable |
|||
:placeholder="$t('device.placeholder.lastAddress')" |
|||
@keyup.enter.native="searchList" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item class="query-compact-item"> |
|||
<el-date-picker |
|||
v-model="range" |
|||
size="mini" |
|||
type="datetimerange" |
|||
value-format="yyyy-MM-dd HH:mm:ss" |
|||
:start-placeholder="$t('device.trajectory.filter.startPlaceholder')" |
|||
:end-placeholder="$t('device.trajectory.filter.endPlaceholder')" |
|||
:clearable="true" |
|||
class="range" |
|||
/> |
|||
</el-form-item> |
|||
|
|||
<div class="query-actions"> |
|||
<el-button type="primary" size="mini" @click="searchList">{{ $t("device.trajectory.page.button.search") }}</el-button> |
|||
<el-button size="mini" @click="resetList">{{ $t("device.trajectory.page.button.reset") }}</el-button> |
|||
</div> |
|||
</el-form> |
|||
|
|||
<div class="device-list" v-loading="loadingList"> |
|||
<el-empty v-if="!loadingList && !items.length" :description="$t('device.trajectory.page.empty.deviceList')" :image-size="66" /> |
|||
<el-scrollbar v-else class="device-scroll"> |
|||
<div |
|||
v-for="item in items" |
|||
:key="item.sn" |
|||
class="device-item" |
|||
:class="{ active: currentItem && currentItem.sn === item.sn }" |
|||
@click="selectItem(item)" |
|||
> |
|||
<div class="device-item__top"> |
|||
<span>{{ item.alias || "-" }}</span> |
|||
</div> |
|||
<div class="device-item__line">{{ $t("device.table.sn") }}: {{ item.sn || "-" }}</div> |
|||
<div class="device-item__line">{{ $t("device.table.lastAddress") }}: {{ item.address || "-" }}</div> |
|||
<div class="device-item__line">{{ $t("device.trajectory.filter.locationTime") }}: {{ formatTime(item.locationTime) }}</div> |
|||
</div> |
|||
</el-scrollbar> |
|||
</div> |
|||
|
|||
<div class="panel-pagination" v-show="total > 0"> |
|||
<pagination |
|||
:total="total" |
|||
:page.sync="query.pageNum" |
|||
:limit.sync="query.pageSize" |
|||
layout="prev, pager, next" |
|||
:pager-count="5" |
|||
:auto-scroll="false" |
|||
@pagination="getList" |
|||
/> |
|||
</div> |
|||
</section> |
|||
|
|||
<section class="panel right"> |
|||
<div class="panel-title">{{ $t("device.trajectory.page.title.detail") }}</div> |
|||
<el-empty v-if="!currentItem" :description="$t('device.trajectory.page.empty.selectDevice')" :image-size="66" /> |
|||
<div v-else class="detail-panel"> |
|||
<el-table :data="detailList" border size="mini" :height="detailTableHeight" v-loading="loadingDetail"> |
|||
<el-table-column type="index" width="50" label="#" /> |
|||
<el-table-column :label="$t('device.table.sn')" prop="sn" min-width="160" show-overflow-tooltip /> |
|||
<el-table-column :label="$t('device.table.lastAddress')" prop="address" min-width="220" show-overflow-tooltip /> |
|||
<el-table-column :label="$t('device.table.coordinates')" min-width="130"> |
|||
<template slot-scope="scope">{{ formatCoordinates(scope.row.lat, scope.row.lng) }}</template> |
|||
</el-table-column> |
|||
<el-table-column :label="$t('device.trajectory.table.time')" min-width="170"> |
|||
<template slot-scope="scope">{{ formatTime(scope.row.locationTime) }}</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { listHistoryTrajectory, listHistoryTrajectoryDetail } from "@/api/device/deviceLocation"; |
|||
|
|||
function getDefaultQuery() { |
|||
return { |
|||
pageNum: 1, |
|||
pageSize: 10, |
|||
sn: null, |
|||
alias: null, |
|||
address: null, |
|||
}; |
|||
} |
|||
|
|||
export default { |
|||
name: "DeviceHistoryTrajectoryPage", |
|||
data() { |
|||
return { |
|||
query: getDefaultQuery(), |
|||
items: [], |
|||
total: 0, |
|||
loadingList: false, |
|||
currentItem: null, |
|||
detailList: [], |
|||
loadingDetail: false, |
|||
range: [], |
|||
availableHeight: 760, |
|||
}; |
|||
}, |
|||
computed: { |
|||
layoutHeight() { |
|||
return Math.max(640, this.availableHeight); |
|||
}, |
|||
detailTableHeight() { |
|||
return Math.max(360, this.layoutHeight - 120); |
|||
}, |
|||
}, |
|||
created() { |
|||
this.initRange(); |
|||
this.getList(); |
|||
}, |
|||
mounted() { |
|||
this.bindViewportResize(); |
|||
}, |
|||
beforeDestroy() { |
|||
this.unbindViewportResize(); |
|||
}, |
|||
methods: { |
|||
initRange() { |
|||
const now = new Date(); |
|||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); |
|||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); |
|||
const format = (date) => this.parseTime(date, "{y}-{m}-{d} {h}:{i}:{s}"); |
|||
this.range = [format(start), format(end)]; |
|||
}, |
|||
bindViewportResize() { |
|||
this.updateViewportHeight(); |
|||
if (typeof window !== "undefined") { |
|||
window.addEventListener("resize", this.updateViewportHeight); |
|||
} |
|||
}, |
|||
unbindViewportResize() { |
|||
if (typeof window !== "undefined") { |
|||
window.removeEventListener("resize", this.updateViewportHeight); |
|||
} |
|||
}, |
|||
updateViewportHeight() { |
|||
if (typeof window === "undefined") return; |
|||
const root = this.$refs.pageRoot; |
|||
const rectTop = root && root.getBoundingClientRect ? root.getBoundingClientRect().top : 0; |
|||
const bottomGap = 12; |
|||
this.availableHeight = window.innerHeight - rectTop - bottomGap; |
|||
}, |
|||
async getList() { |
|||
this.loadingList = true; |
|||
try { |
|||
const res = await listHistoryTrajectory(this.buildQueryParams()); |
|||
const data = (res && res.data) || res || {}; |
|||
const list = Array.isArray(data.list) ? data.list : Array.isArray(data.rows) ? data.rows : []; |
|||
this.items = list; |
|||
this.total = Number(data.total || 0); |
|||
if (this.items.length) { |
|||
if (!this.currentItem || !this.items.find((item) => item.sn === this.currentItem.sn)) { |
|||
await this.selectItem(this.items[0]); |
|||
} |
|||
} else { |
|||
this.currentItem = null; |
|||
this.detailList = []; |
|||
} |
|||
} catch (error) { |
|||
this.items = []; |
|||
this.total = 0; |
|||
this.currentItem = null; |
|||
this.detailList = []; |
|||
this.$message.error((error && error.message) || this.$t("device.trajectory.message.trajectoryLoadFailed")); |
|||
} finally { |
|||
this.loadingList = false; |
|||
} |
|||
}, |
|||
buildQueryParams() { |
|||
const params = { ...this.query }; |
|||
if (Array.isArray(this.range) && this.range.length === 2) { |
|||
params.startLocationTime = this.range[0]; |
|||
params.endLocationTime = this.range[1]; |
|||
} |
|||
return params; |
|||
}, |
|||
searchList() { |
|||
this.query.pageNum = 1; |
|||
this.getList(); |
|||
}, |
|||
resetList() { |
|||
this.query = getDefaultQuery(); |
|||
this.initRange(); |
|||
this.getList(); |
|||
}, |
|||
async selectItem(item) { |
|||
this.currentItem = item; |
|||
await this.loadDetail(); |
|||
}, |
|||
async loadDetail() { |
|||
if (!this.currentItem || !this.currentItem.sn) { |
|||
this.detailList = []; |
|||
return; |
|||
} |
|||
this.loadingDetail = true; |
|||
try { |
|||
const res = await listHistoryTrajectoryDetail({ |
|||
sn: this.currentItem.sn, |
|||
alias: this.query.alias || undefined, |
|||
address: this.query.address || undefined, |
|||
startLocationTime: this.range && this.range.length ? this.range[0] : undefined, |
|||
endLocationTime: this.range && this.range.length ? this.range[1] : undefined, |
|||
}); |
|||
const data = (res && res.data) || res || {}; |
|||
this.detailList = Array.isArray(data.list) ? data.list : Array.isArray(data.rows) ? data.rows : []; |
|||
} catch (error) { |
|||
this.detailList = []; |
|||
this.$message.error((error && error.message) || this.$t("device.trajectory.message.trajectoryLoadFailed")); |
|||
} finally { |
|||
this.loadingDetail = false; |
|||
} |
|||
}, |
|||
formatTime(value) { |
|||
if (!value) { |
|||
return "-"; |
|||
} |
|||
return this.parseTime ? this.parseTime(value, "{y}-{m}-{d} {h}:{i}:{s}") : String(value); |
|||
}, |
|||
normalizeCoordinate(value, type) { |
|||
if (value === undefined || value === null || value === "") { |
|||
return null; |
|||
} |
|||
const numberValue = Number(value); |
|||
if (!Number.isFinite(numberValue)) { |
|||
return null; |
|||
} |
|||
const directLimit = type === "lat" ? 90 : 180; |
|||
const normalizedValue = Math.abs(numberValue) > directLimit ? numberValue / 10000000 : numberValue; |
|||
if (Math.abs(normalizedValue) > directLimit) { |
|||
return null; |
|||
} |
|||
return Number(normalizedValue.toFixed(7)); |
|||
}, |
|||
formatCoordinateValue(value) { |
|||
if (value === null || value === undefined) { |
|||
return "-"; |
|||
} |
|||
const strValue = String(value).trim(); |
|||
if (!strValue || strValue === "-") { |
|||
return "-"; |
|||
} |
|||
const match = strValue.match(/^([+-]?\d+)(?:\.(\d+))?$/); |
|||
if (!match) { |
|||
return "-"; |
|||
} |
|||
const integerPart = match[1]; |
|||
const decimalPart = match[2] || ""; |
|||
const fixedDecimal = decimalPart.slice(0, 2).padEnd(2, "0"); |
|||
return `${integerPart}.${fixedDecimal}`; |
|||
}, |
|||
formatCoordinates(lat, lng) { |
|||
const latNum = this.normalizeCoordinate(lat, "lat"); |
|||
const lngNum = this.normalizeCoordinate(lng, "lng"); |
|||
return `${this.formatCoordinateValue(latNum)} / ${this.formatCoordinateValue(lngNum)}`; |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.history-trajectory-layout { |
|||
display: grid; |
|||
grid-template-columns: 320px minmax(0, 1fr); |
|||
gap: 14px; |
|||
height: 760px; |
|||
align-items: stretch; |
|||
} |
|||
|
|||
.panel { |
|||
background: #fff; |
|||
border: 1px solid #e8edf3; |
|||
border-radius: 10px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 10px rgba(31, 45, 61, .05); |
|||
} |
|||
|
|||
.left { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
min-height: 0; |
|||
} |
|||
|
|||
.right { |
|||
height: 100%; |
|||
min-height: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.panel-title { |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
color: #1f2d3d; |
|||
padding: 12px 14px; |
|||
border-bottom: 1px solid #eef2f7; |
|||
background: linear-gradient(180deg, #f8fbff 0, #fff 100%); |
|||
} |
|||
|
|||
.query-form { |
|||
padding: 2px 10px 0; |
|||
} |
|||
|
|||
.query-form .query-compact-item { |
|||
margin-bottom: 4px !important; |
|||
} |
|||
|
|||
.query-form :deep(.el-form-item__content) { |
|||
line-height: 28px !important; |
|||
} |
|||
|
|||
.query-form :deep(.el-input--mini .el-input__inner) { |
|||
height: 28px !important; |
|||
line-height: 28px !important; |
|||
padding: 0 10px; |
|||
} |
|||
|
|||
.query-form :deep(.el-input--mini .el-input__icon) { |
|||
line-height: 28px !important; |
|||
} |
|||
|
|||
.query-actions { |
|||
display: flex; |
|||
gap: 8px; |
|||
margin-bottom: 2px; |
|||
} |
|||
|
|||
.query-actions :deep(.el-button--mini) { |
|||
padding: 3px 8px; |
|||
} |
|||
|
|||
.range { |
|||
width: 100%; |
|||
} |
|||
|
|||
.device-list { |
|||
flex: 1; |
|||
min-height: 0; |
|||
padding: 0 10px 10px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.device-scroll { |
|||
height: 100%; |
|||
} |
|||
|
|||
.panel-pagination { |
|||
flex: 0 0 auto; |
|||
padding: 6px 10px 8px; |
|||
border-top: 1px solid #eef2f7; |
|||
background: #fff; |
|||
} |
|||
|
|||
.panel-pagination :deep(.pagination-container) { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
.panel-pagination :deep(.el-pagination) { |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.device-item { |
|||
border: 1px solid #eef2f7; |
|||
border-radius: 8px; |
|||
padding: 10px 12px; |
|||
margin-bottom: 10px; |
|||
cursor: pointer; |
|||
transition: all .2s ease; |
|||
} |
|||
|
|||
.device-item:hover { |
|||
border-color: #c6e2ff; |
|||
box-shadow: 0 2px 8px rgba(64, 158, 255, .12); |
|||
} |
|||
|
|||
.device-item.active { |
|||
border-color: #409eff; |
|||
background: #ecf5ff; |
|||
} |
|||
|
|||
.device-item__top { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-bottom: 6px; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
color: #303133; |
|||
} |
|||
|
|||
.device-item__line { |
|||
font-size: 12px; |
|||
line-height: 1.6; |
|||
color: #606266; |
|||
} |
|||
|
|||
.detail-panel { |
|||
padding: 12px; |
|||
flex: 1; |
|||
min-height: 0; |
|||
} |
|||
|
|||
.detail-panel :deep(.el-table) { |
|||
height: 100%; |
|||
} |
|||
|
|||
@media (max-width: 1280px) { |
|||
.history-trajectory-layout { |
|||
grid-template-columns: 1fr; |
|||
height: auto; |
|||
min-height: auto; |
|||
} |
|||
|
|||
.left, |
|||
.right { |
|||
height: auto; |
|||
min-height: 0; |
|||
} |
|||
|
|||
.device-list { |
|||
min-height: 320px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,14 @@ |
|||
<template> |
|||
<FenceDeviceIndex menu-mode="alarm" /> |
|||
</template> |
|||
|
|||
<script> |
|||
import FenceDeviceIndex from './index.vue' |
|||
|
|||
export default { |
|||
name: 'FenceAlarmCenterPage', |
|||
components: { |
|||
FenceDeviceIndex |
|||
} |
|||
} |
|||
</script> |
|||
@ -0,0 +1,14 @@ |
|||
<template> |
|||
<FenceDeviceIndex menu-mode="fence" /> |
|||
</template> |
|||
|
|||
<script> |
|||
import FenceDeviceIndex from './index.vue' |
|||
|
|||
export default { |
|||
name: 'FenceManagePage', |
|||
components: { |
|||
FenceDeviceIndex |
|||
} |
|||
} |
|||
</script> |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue