Browse Source

b端国际化 围栏

master
hx 4 weeks ago
parent
commit
495dc4157f
  1. 115
      src/api/device/fenceDevice.js
  2. 257
      src/lang/fence-messages.js
  3. 73
      src/utils/loadLeafletDraw.js
  4. 462
      src/views/device/device/history/index.vue
  5. 14
      src/views/fence/device/alarmCenter.vue
  6. 14
      src/views/fence/device/fenceManage.vue
  7. 3344
      src/views/fence/device/index.vue

115
src/api/device/fenceDevice.js

@ -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'
})
}

257
src/lang/fence-messages.js

@ -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

73
src/utils/loadLeafletDraw.js

@ -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;
}

462
src/views/device/device/history/index.vue

@ -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>

14
src/views/fence/device/alarmCenter.vue

@ -0,0 +1,14 @@
<template>
<FenceDeviceIndex menu-mode="alarm" />
</template>
<script>
import FenceDeviceIndex from './index.vue'
export default {
name: 'FenceAlarmCenterPage',
components: {
FenceDeviceIndex
}
}
</script>

14
src/views/fence/device/fenceManage.vue

@ -0,0 +1,14 @@
<template>
<FenceDeviceIndex menu-mode="fence" />
</template>
<script>
import FenceDeviceIndex from './index.vue'
export default {
name: 'FenceManagePage',
components: {
FenceDeviceIndex
}
}
</script>

3344
src/views/fence/device/index.vue

File diff suppressed because it is too large
Loading…
Cancel
Save