电脑端新增区域标注管理功能

master
赵昌 2 weeks ago
parent 080a9c24c4
commit 81619618ef

@ -249,6 +249,10 @@ body {
.soldier-name { font-size: 13px; font-weight: 600; color: #333; }
.soldier-coord { font-size: 11px; color: #999; }
/* ===== 区域标注管理 ===== */
.zone-actions-card { flex-shrink: 0; margin-bottom: 2px; }
.zone-actions-card .card-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; }
/* ===== 危险区域列表 ===== */
.danger-card { flex-shrink: 0; }
.card-badge.danger { background: rgba(255, 77, 79, 0.15); color: #ff4d4f; }

@ -162,6 +162,12 @@
</div>
<div id="soldier-list" class="list-scroll-content"></div>
</div>
<div class="card zone-actions-card">
<div class="card-header">
<span class="card-title">区域标注管理</span>
<button class="btn-link" id="btn-add-zone" style="font-size:12px;">+ 添加区域</button>
</div>
</div>
<div class="card danger-card">
<div class="card-header">
<span class="card-title">危险区域</span>
@ -188,6 +194,57 @@
</div>
</div>
<!-- ===== 添加区域弹窗 ===== -->
<div class="modal-overlay" id="modal-zone" style="display:none">
<div class="modal" style="max-width:420px;">
<div class="modal-header">
<span class="modal-title">添加区域标注</span>
<button class="modal-close" id="modal-zone-close">&times;</button>
</div>
<div class="modal-body" id="modal-zone-body">
<div class="form-row">
<label>区域名称</label>
<input type="text" id="zone-name" placeholder="如:东侧路口" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;box-sizing:border-box;" />
</div>
<div class="form-row" style="margin-top:10px;">
<label>类型</label>
<div style="display:flex;gap:10px;margin-top:4px;">
<label style="flex:1;text-align:center;padding:6px;border:2px solid #ff4d4f;border-radius:6px;font-size:13px;color:#ff4d4f;cursor:pointer;">
<input type="radio" name="ztype" value="danger" checked /> 危险区域
</label>
<label style="flex:1;text-align:center;padding:6px;border:2px solid #52c41a;border-radius:6px;font-size:13px;color:#52c41a;cursor:pointer;">
<input type="radio" name="ztype" value="safe" /> 安全区域
</label>
</div>
</div>
<div class="form-row" style="margin-top:10px;">
<label>位置</label>
<div style="display:flex;gap:8px;align-items:center;">
<input type="text" id="zone-coords" placeholder="点击地图选择" readonly style="flex:1;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#f9f9f9;box-sizing:border-box;cursor:pointer;" onclick="UIModule.pickZoneFromMap()" />
<button class="btn btn-secondary" id="btn-zone-pick" onclick="UIModule.pickZoneFromMap()" style="padding:8px 12px;flex-shrink:0;">🗺️ 选点</button>
</div>
<div style="font-size:11px;color:#999;margin-top:4px;">点击「选点」后在地图上单击选择位置</div>
</div>
<div class="form-row" style="margin-top:8px;">
<label>纬度</label>
<input type="number" step="0.0001" id="zone-lat" placeholder="28.2280" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;box-sizing:border-box;" />
</div>
<div class="form-row" style="margin-top:8px;">
<label>经度</label>
<input type="number" step="0.0001" id="zone-lng" placeholder="112.9388" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;box-sizing:border-box;" />
</div>
<div class="form-row" style="margin-top:8px;">
<label>半径(米)</label>
<input type="number" id="zone-radius" value="100" min="10" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;box-sizing:border-box;" />
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modal-zone-cancel">取消</button>
<button class="btn btn-primary" id="modal-zone-confirm">确认添加</button>
</div>
</div>
</div>
<!-- ===== 调度弹窗 ===== -->
<div class="modal-overlay" id="modal-task" style="display:none">
<div class="modal">

@ -17,6 +17,9 @@ const MapModule = (() => {
let dangerLayerGroup = null;
let safeZoneItems = []; // [ { circle, marker } ]
let safeLayerGroup = null;
let zonePickActive = false; // 区域选点模式
let zonePickMarker = null; // 选点标记
let zonePickCallback = null; // 选点回调
const DEFAULT_LAT = 28.2280; // 长沙市区
const DEFAULT_LNG = 112.9388;
@ -51,6 +54,9 @@ const MapModule = (() => {
// 点击地图添加航点
map.on('click', onMapClick);
// 区域标注选点模式(由 addZone 触发)
map.on('click', onZonePick);
// 初始化航线(空折线)
polyline = L.polyline([], {
color: '#007aff',
@ -93,9 +99,45 @@ const MapModule = (() => {
}
function onMapClick(e) {
// 选点模式下不添加航点
if (zonePickActive) return;
addWaypoint(e.latlng.lat, e.latlng.lng);
}
function onZonePick(e) {
if (!zonePickActive) return;
var lat = e.latlng.lat;
var lng = e.latlng.lng;
zonePickActive = false;
// 恢复光标
if (map) map.getContainer().style.cursor = '';
// 清除旧标记
if (zonePickMarker) { map.removeLayer(zonePickMarker); zonePickMarker = null; }
// 添加新标记
zonePickMarker = L.marker([lat, lng], {
icon: L.divIcon({ className: 'zone-pick-marker', html: '<div style="background:#1890ff;border:3px solid #fff;width:16px;height:16px;border-radius:50%;box-shadow:0 0 8px rgba(0,0,0,0.3);"></div>', iconSize: [16, 16], iconAnchor: [8, 8] })
}).addTo(map);
setTimeout(function() {
if (zonePickMarker) { map.removeLayer(zonePickMarker); zonePickMarker = null; }
}, 3000);
// 触发回调
if (zonePickCallback) { zonePickCallback(lat, lng); zonePickCallback = null; }
}
function startZonePick(callback) {
if (!map) { alert('请先进入无人机监控页面加载地图'); return; }
zonePickActive = true;
zonePickCallback = callback;
if (map) map.getContainer().style.cursor = 'crosshair';
}
function stopZonePick() {
zonePickActive = false;
zonePickCallback = null;
if (zonePickMarker) { if (map) map.removeLayer(zonePickMarker); zonePickMarker = null; }
if (map) map.getContainer().style.cursor = '';
}
function addWaypoint(lat, lng) {
const defaultAlt = parseInt(document.getElementById('default-alt').value) || 15;
const defaultHold = parseInt(document.getElementById('default-hold').value) || 0;
@ -323,6 +365,8 @@ const MapModule = (() => {
addSafeZone,
clearAllDangerZones,
clearAllSafeZones,
startZonePick,
stopZonePick,
DEFAULT_LAT,
DEFAULT_LNG
};

@ -99,6 +99,15 @@ const UIModule = (() => {
fetchZones();
setInterval(fetchZones, 5000);
// 区域标注管理
document.getElementById('btn-add-zone').addEventListener('click', openZoneModal);
document.getElementById('modal-zone-close').addEventListener('click', closeZoneModal);
document.getElementById('modal-zone-cancel').addEventListener('click', closeZoneModal);
document.getElementById('modal-zone-confirm').addEventListener('click', addZone);
document.getElementById('modal-zone').addEventListener('click', function(e) {
if (e.target === e.currentTarget) closeZoneModal();
});
addLog('info', '系统就绪 — 实机连接模式');
addLog('info', '请确保电脑已连接 P600 的 WiFi 数传rosbridge 需在机载电脑上运行');
}
@ -590,12 +599,16 @@ const UIModule = (() => {
return;
}
container.innerHTML = dangerZones.map(function(dz) {
return '<div class="danger-item">' +
'<span class="danger-icon">&#9888;</span>' +
'<div class="danger-info">' +
'<div class="danger-desc">' + (dz.description || '危险区域') + '</div>' +
'<div class="danger-coord">(' + Number(dz.lat).toFixed(4) + ', ' + Number(dz.lng).toFixed(4) + ') R' + (dz.radius || 50) + 'm</div>' +
var zid = dz.id || '';
return '<div class="danger-item" style="justify-content:space-between;">' +
'<div style="display:flex;align-items:center;gap:10px;min-width:0;flex:1;">' +
'<span class="danger-icon">&#9888;</span>' +
'<div class="danger-info">' +
'<div class="danger-desc">' + (dz.description || '危险区域') + '</div>' +
'<div class="danger-coord">(' + Number(dz.lat).toFixed(4) + ', ' + Number(dz.lng).toFixed(4) + ') R' + (dz.radius || 50) + 'm</div>' +
'</div>' +
'</div>' +
(zid ? '<span style="color:#ff4d4f;font-size:12px;cursor:pointer;padding:4px;flex-shrink:0;" onclick="UIModule.deleteZone(' + zid + ')">&#128465;</span>' : '') +
'</div>';
}).join('');
}
@ -611,16 +624,104 @@ const UIModule = (() => {
return;
}
container.innerHTML = safeZones.map(function(sz) {
return '<div class="safe-item">' +
'<span class="safe-icon">&#10004;</span>' +
'<div class="danger-info">' +
'<div class="danger-desc">' + (sz.description || '安全区域') + '</div>' +
'<div class="danger-coord">(' + Number(sz.lat).toFixed(4) + ', ' + Number(sz.lng).toFixed(4) + ') R' + (sz.radius || 50) + 'm</div>' +
var zid = sz.id || '';
return '<div class="safe-item" style="justify-content:space-between;">' +
'<div style="display:flex;align-items:center;gap:10px;min-width:0;flex:1;">' +
'<span class="safe-icon">&#10004;</span>' +
'<div class="danger-info">' +
'<div class="danger-desc">' + (sz.description || '安全区域') + '</div>' +
'<div class="danger-coord">(' + Number(sz.lat).toFixed(4) + ', ' + Number(sz.lng).toFixed(4) + ') R' + (sz.radius || 50) + 'm</div>' +
'</div>' +
'</div>' +
(zid ? '<span style="color:#ff4d4f;font-size:12px;cursor:pointer;padding:4px;flex-shrink:0;" onclick="UIModule.deleteZone(' + zid + ')">&#128465;</span>' : '') +
'</div>';
}).join('');
}
// =============== 区域标注管理 ===============
function openZoneModal(keepValues) {
if (!keepValues) {
document.getElementById('zone-name').value = '';
document.getElementById('zone-coords').value = '';
document.getElementById('zone-lat').value = '';
document.getElementById('zone-lng').value = '';
document.getElementById('zone-radius').value = '100';
}
MapModule.stopZonePick();
var radios = document.querySelectorAll('#modal-zone input[name="ztype"]');
if (radios.length > 0) radios[0].checked = true;
document.getElementById('modal-zone').style.display = 'flex';
}
function closeZoneModal() {
MapModule.stopZonePick();
document.getElementById('modal-zone').style.display = 'none';
}
function pickZoneFromMap() {
if (!mapInited) {
addLog('warning', '请先进入无人机监控页面加载地图');
return;
}
closeZoneModal();
addLog('info', '请在地图上点击选择区域位置');
MapModule.startZonePick(function(lat, lng) {
document.getElementById('zone-coords').value = lat.toFixed(6) + ', ' + lng.toFixed(6);
document.getElementById('zone-lat').value = lat.toFixed(6);
document.getElementById('zone-lng').value = lng.toFixed(6);
openZoneModal(true); // 传入 true 保留已填入的坐标
});
}
async function addZone() {
var name = document.getElementById('zone-name').value.trim();
var lat = parseFloat(document.getElementById('zone-lat').value);
var lng = parseFloat(document.getElementById('zone-lng').value);
var radius = parseInt(document.getElementById('zone-radius').value) || 100;
var typeEl = document.querySelector('#modal-zone input[name="ztype"]:checked');
var type = typeEl ? typeEl.value : 'danger';
if (!name) { addLog('warning', '请输入区域名称'); return; }
if (isNaN(lat) || isNaN(lng)) { addLog('warning', '请输入有效的经纬度'); return; }
try {
var resp = await fetch('/api/zones', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: name,
lat: lat, lng: lng,
radius: radius,
zone_type: type
})
});
if (resp.ok) {
addLog('success', '已添加' + (type === 'danger' ? '危险区域' : '安全区域') + ': ' + name);
closeZoneModal();
fetchZones();
if (mapInited) {
if (type === 'danger') MapModule.addDangerZone(lat, lng, radius, name);
else MapModule.addSafeZone(lat, lng, radius, name);
}
}
} catch (e) {
addLog('error', '添加区域失败: 网络错误');
}
}
async function deleteZone(id) {
try {
var resp = await fetch('/api/zones/' + id, { method: 'DELETE' });
if (resp.ok) {
addLog('info', '已删除区域 #' + id);
fetchZones();
}
} catch (e) {
addLog('error', '删除失败: 网络错误');
}
}
// =============== 闪光检测功能 ===============
function onEnableFlashDetection() {
@ -861,6 +962,8 @@ const UIModule = (() => {
onTelemetry,
onStatus,
onDroneDisconnected,
deleteZone,
pickZoneFromMap,
updateWaypointList,
updateButtons,
addLog

@ -306,8 +306,8 @@ def get_zones():
@app.route("/api/zones", methods=["POST"])
@require_auth
def add_zone():
"""添加区域标注(电脑端管理,不需要 auth"""
data = request.get_json(force=True)
zone_type = data.get("zone_type", "danger")
if zone_type not in ("danger", "safe"):
@ -363,6 +363,16 @@ def add_danger_zone_legacy():
return jsonify({"ok": True, "id": zone_id})
@app.route("/api/zones/<zone_id>", methods=["DELETE"])
def delete_zone(zone_id):
"""删除区域标注"""
conn = get_db()
conn.execute("DELETE FROM zones WHERE id=?", (zone_id,))
conn.commit()
conn.close()
return jsonify({"ok": True})
# ===== REST API: 物资需求单兵APP =====
def _next_demand_id():

Loading…
Cancel
Save