feat(gps): клик по точке объекта показывает название (попап + «Открыть задачу»); точки объектов кластеризуются, список кластера вертикальный

This commit is contained in:
2026-09-22 22:11:38 +03:00
parent dfedb3ebfa
commit 27a14ec513
5 changed files with 24 additions and 7 deletions

View File

@@ -26,6 +26,10 @@ export interface GpsMapMarker {
label?: string;
/** Вторая строка подписи (заказчик) */
label2?: string;
/** Участвовать в кластеризации (кроме kind vehicle/person) */
clusterable?: boolean;
/** Название для списка кластера (когда label не задан) */
clusterLabel?: string;
/** Приглушённый маркер для offline-объектов */
offline?: boolean;
/** Примерная позиция (LBS по вышкам, нет GPS): полупрозрачный маркер с пунктирной обводкой */

View File

@@ -175,7 +175,9 @@ export function OsmMap({
zIndexOffset: m.zIndex ?? 0,
gpsMarkerId: m.id,
} as GpsMarkerOptions);
if (m.onClick) {
// Клик по маркеру: если задан popup — открываем его (показ названия),
// onClick вызывается только из элементов внутри popup (делегирование)
if (m.onClick && !m.popup) {
leafletMarker.on('click', () => {
markersByIdRef.current.get(String(m.id))?.onClick?.();
});
@@ -185,7 +187,7 @@ export function OsmMap({
leafletMarker.bindPopup(m.popup);
}
// Кластерим только объекты; dot-маркеры и noCluster — в обычный слой
const clusterable = (m.kind === 'vehicle' || m.kind === 'person') && !m.noCluster;
const clusterable = ((m.kind === 'vehicle' || m.kind === 'person') || m.clusterable === true) && !m.noCluster;
if (clusterable) {
leafletMarker.addTo(clusterLayer);
} else {

View File

@@ -260,7 +260,9 @@ export function Yandex21Map({
...(m.zIndex !== undefined ? { zIndex: m.zIndex } : {}),
}
);
if (m.onClick) {
// Клик по маркеру: если задан popup (balloonContent) — открываем его,
// onClick вызывается только из элементов внутри balloon (делегирование)
if (m.onClick && !m.popup) {
placemark.events.add('click', () => {
markersByIdRef.current.get(String(m.id))?.onClick?.();
});
@@ -271,7 +273,7 @@ export function Yandex21Map({
for (const m of markers) {
// Кластерим только объекты (vehicle/person); dot-маркеры и noCluster —
// в обычную коллекцию (точки трека, старт/финиш, вершины геозон)
const clusterable = (m.kind === 'vehicle' || m.kind === 'person') && !m.noCluster;
const clusterable = ((m.kind === 'vehicle' || m.kind === 'person') || m.clusterable === true) && !m.noCluster;
const placemark = createPlacemark(m);
placemarksRef.current.set(String(m.id), placemark);
if (clusterable) {

View File

@@ -121,7 +121,7 @@ export function gpsClusterListHtml(markers: GpsMapMarker[]): string {
.map(
(m) => `<button type="button" data-gps-marker-id="${escapeHtml(String(m.id))}" style="display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border:0;background:none;cursor:pointer;font-size:12px;text-align:left;border-radius:4px;">
<span style="width:10px;height:10px;border-radius:50%;background:${m.color || DEFAULT_COLOR};border:1px solid #fff;box-shadow:0 0 1px rgba(0,0,0,0.4);flex-shrink:0;"></span>
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(m.label || 'Объект')}</span>
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(m.clusterLabel || m.label || 'Объект')}</span>
</button>`
)
.join('');

View File

@@ -228,10 +228,15 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
const taskMarkers: GpsMapMarker[] = useMemo(() => {
if (!tasksLayer || mapFieldId === null) return [];
const esc = (t: string) => t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const markers: GpsMapMarker[] = [];
for (const t of tasksData?.tasks ?? []) {
const geo = parseGeo(t.fieldValues?.[mapFieldId]);
if (!geo) continue;
const st = shortTitle(t.title);
// Попап по клику: название (две строки) + кнопка «Открыть задачу».
// Работает и при выключенных подписях — название видно по клику.
const popup = `<div style="max-width:220px;"><b>${esc(st.line1)}</b>${st.line2 ? `<div style="color:#52525b;font-size:11px;">${esc(st.line2)}</div>` : ''}<button type="button" data-gps-marker-id="task-${t.id}" style="margin-top:6px;font-size:11px;color:#2563eb;background:none;border:0;cursor:pointer;padding:0;">Открыть задачу</button></div>`;
markers.push({
id: `task-${t.id}`,
lat: geo.lat,
@@ -240,10 +245,14 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
color: '#8b5cf6',
// Подпись — только при включённом режиме «Названия», в две строки
...(showLabels
? (() => { const st = shortTitle(t.title); return { label: st.line1, ...(st.line2 ? { label2: st.line2 } : {}) }; })()
? { label: st.line1, ...(st.line2 ? { label2: st.line2 } : {}) }
: {}),
noCluster: true,
// Кластеризация как у машин: при отдалении — кружок с числом,
// клик по кластеру — вертикальный список названий
clusterable: true,
clusterLabel: t.title,
zIndex: 40,
popup,
onClick: rulerMode ? undefined : () => setLocation(`/forms/${mapFormId}/tasks/${t.id}`),
});
}