feat(gps): клик по точке объекта показывает название (попап + «Открыть задачу»); точки объектов кластеризуются, список кластера вертикальный
This commit is contained in:
@@ -26,6 +26,10 @@ export interface GpsMapMarker {
|
||||
label?: string;
|
||||
/** Вторая строка подписи (заказчик) */
|
||||
label2?: string;
|
||||
/** Участвовать в кластеризации (кроме kind vehicle/person) */
|
||||
clusterable?: boolean;
|
||||
/** Название для списка кластера (когда label не задан) */
|
||||
clusterLabel?: string;
|
||||
/** Приглушённый маркер для offline-объектов */
|
||||
offline?: boolean;
|
||||
/** Примерная позиция (LBS по вышкам, нет GPS): полупрозрачный маркер с пунктирной обводкой */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
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}`),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user