feat(gps): клик по точке объекта показывает название (попап + «Открыть задачу»); точки объектов кластеризуются, список кластера вертикальный
This commit is contained in:
@@ -26,6 +26,10 @@ export interface GpsMapMarker {
|
|||||||
label?: string;
|
label?: string;
|
||||||
/** Вторая строка подписи (заказчик) */
|
/** Вторая строка подписи (заказчик) */
|
||||||
label2?: string;
|
label2?: string;
|
||||||
|
/** Участвовать в кластеризации (кроме kind vehicle/person) */
|
||||||
|
clusterable?: boolean;
|
||||||
|
/** Название для списка кластера (когда label не задан) */
|
||||||
|
clusterLabel?: string;
|
||||||
/** Приглушённый маркер для offline-объектов */
|
/** Приглушённый маркер для offline-объектов */
|
||||||
offline?: boolean;
|
offline?: boolean;
|
||||||
/** Примерная позиция (LBS по вышкам, нет GPS): полупрозрачный маркер с пунктирной обводкой */
|
/** Примерная позиция (LBS по вышкам, нет GPS): полупрозрачный маркер с пунктирной обводкой */
|
||||||
|
|||||||
@@ -175,7 +175,9 @@ export function OsmMap({
|
|||||||
zIndexOffset: m.zIndex ?? 0,
|
zIndexOffset: m.zIndex ?? 0,
|
||||||
gpsMarkerId: m.id,
|
gpsMarkerId: m.id,
|
||||||
} as GpsMarkerOptions);
|
} as GpsMarkerOptions);
|
||||||
if (m.onClick) {
|
// Клик по маркеру: если задан popup — открываем его (показ названия),
|
||||||
|
// onClick вызывается только из элементов внутри popup (делегирование)
|
||||||
|
if (m.onClick && !m.popup) {
|
||||||
leafletMarker.on('click', () => {
|
leafletMarker.on('click', () => {
|
||||||
markersByIdRef.current.get(String(m.id))?.onClick?.();
|
markersByIdRef.current.get(String(m.id))?.onClick?.();
|
||||||
});
|
});
|
||||||
@@ -185,7 +187,7 @@ export function OsmMap({
|
|||||||
leafletMarker.bindPopup(m.popup);
|
leafletMarker.bindPopup(m.popup);
|
||||||
}
|
}
|
||||||
// Кластерим только объекты; dot-маркеры и noCluster — в обычный слой
|
// Кластерим только объекты; 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) {
|
if (clusterable) {
|
||||||
leafletMarker.addTo(clusterLayer);
|
leafletMarker.addTo(clusterLayer);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -260,7 +260,9 @@ export function Yandex21Map({
|
|||||||
...(m.zIndex !== undefined ? { zIndex: m.zIndex } : {}),
|
...(m.zIndex !== undefined ? { zIndex: m.zIndex } : {}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (m.onClick) {
|
// Клик по маркеру: если задан popup (balloonContent) — открываем его,
|
||||||
|
// onClick вызывается только из элементов внутри balloon (делегирование)
|
||||||
|
if (m.onClick && !m.popup) {
|
||||||
placemark.events.add('click', () => {
|
placemark.events.add('click', () => {
|
||||||
markersByIdRef.current.get(String(m.id))?.onClick?.();
|
markersByIdRef.current.get(String(m.id))?.onClick?.();
|
||||||
});
|
});
|
||||||
@@ -271,7 +273,7 @@ export function Yandex21Map({
|
|||||||
for (const m of markers) {
|
for (const m of markers) {
|
||||||
// Кластерим только объекты (vehicle/person); dot-маркеры и noCluster —
|
// Кластерим только объекты (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);
|
const placemark = createPlacemark(m);
|
||||||
placemarksRef.current.set(String(m.id), placemark);
|
placemarksRef.current.set(String(m.id), placemark);
|
||||||
if (clusterable) {
|
if (clusterable) {
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function gpsClusterListHtml(markers: GpsMapMarker[]): string {
|
|||||||
.map(
|
.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;">
|
(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="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>`
|
</button>`
|
||||||
)
|
)
|
||||||
.join('');
|
.join('');
|
||||||
|
|||||||
@@ -228,10 +228,15 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
|
|||||||
|
|
||||||
const taskMarkers: GpsMapMarker[] = useMemo(() => {
|
const taskMarkers: GpsMapMarker[] = useMemo(() => {
|
||||||
if (!tasksLayer || mapFieldId === null) return [];
|
if (!tasksLayer || mapFieldId === null) return [];
|
||||||
|
const esc = (t: string) => t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
const markers: GpsMapMarker[] = [];
|
const markers: GpsMapMarker[] = [];
|
||||||
for (const t of tasksData?.tasks ?? []) {
|
for (const t of tasksData?.tasks ?? []) {
|
||||||
const geo = parseGeo(t.fieldValues?.[mapFieldId]);
|
const geo = parseGeo(t.fieldValues?.[mapFieldId]);
|
||||||
if (!geo) continue;
|
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({
|
markers.push({
|
||||||
id: `task-${t.id}`,
|
id: `task-${t.id}`,
|
||||||
lat: geo.lat,
|
lat: geo.lat,
|
||||||
@@ -240,10 +245,14 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
|
|||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
// Подпись — только при включённом режиме «Названия», в две строки
|
// Подпись — только при включённом режиме «Названия», в две строки
|
||||||
...(showLabels
|
...(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,
|
zIndex: 40,
|
||||||
|
popup,
|
||||||
onClick: rulerMode ? undefined : () => setLocation(`/forms/${mapFormId}/tasks/${t.id}`),
|
onClick: rulerMode ? undefined : () => setLocation(`/forms/${mapFormId}/tasks/${t.id}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user