Files
wiki/tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md
2026-05-05 18:10:01 +03:00

11 KiB
Raw Permalink Blame History

Dev Task: Enduro Trails — Маркеры и редизайн панели маршрута

Файлы: /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/ Деплой: node /tmp/deploy_static.js Бэкенд не трогать.


Часть 1: Маркеры точек на карте

Текущее состояние

  • Старт: зелёный круг «A»
  • Финиш: красный круг «B»
  • Промежуточные: белый круг с синей рамкой, цифра

Новый дизайн маркеров

Старт (index === 0): флаг-пин зелёный с буквой «S» Финиш (index === total-1): флаг-пин красный с буквой «F» Промежуточные: синий пин с цифрой 1, 2, 3...

Форма пина (капля/слеза, как у Google Maps):

  ╭───╮
  │ S │
  ╰─┬─╯
    │

Реализация через SVG в innerHTML маркера:

function createWaypointMarkerEl(index, total) {
  const el = document.createElement('div');
  el.className = 'route-waypoint-marker marker-anim';
  el.style.cssText = 'cursor: grab; width: 28px; height: 36px; position: relative;';

  let bg, label;
  if (index === 0) {
    bg = '#2EA043'; label = 'S';
  } else if (index === total - 1) {
    bg = '#FF3B1F'; label = 'F';
  } else {
    bg = '#0066ff'; label = String(index);
  }

  el.innerHTML = `<svg width="28" height="36" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M14 0C6.268 0 0 6.268 0 14C0 24.5 14 36 14 36C14 36 28 24.5 28 14C28 6.268 21.732 0 14 0Z" fill="${bg}"/>
    <path d="M14 1C6.82 1 1 6.82 1 14C1 24 14 35 14 35C14 35 27 24 27 14C27 6.82 21.18 1 14 1Z" fill="${bg}" stroke="white" stroke-width="1.5"/>
    <text x="14" y="19" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="${label.length > 1 ? '9' : '11'}" font-weight="700" fill="white">${label}</text>
  </svg>`;

  return el;
}

CSS для маркера (добавить в app.css):

.route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); }
.route-waypoint-marker:active { cursor: grabbing; }

Часть 2: Список точек в панели — редизайн

Текущее состояние

● 55.723, 37.612  [×]
● 56.123, 38.234  [×]

Новый дизайн

[S] Москва, Тверская ул.        [×]
[1] Клин                        [×]
[F] Тверь, центр                [×]

Иконка слева — мини-версия пина (SVG, 20×26px), цвет соответствует маркеру на карте.

function waypointPinSvg(label, color) {
  const fs = label.length > 1 ? '7' : '9';
  return `<svg width="20" height="26" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M14 1C6.82 1 1 6.82 1 14C1 24 14 35 14 35C14 35 27 24 27 14C27 6.82 21.18 1 14 1Z" fill="${color}" stroke="white" stroke-width="1.5"/>
    <text x="14" y="19" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="${fs}" font-weight="700" fill="white">${label}</text>
  </svg>`;
}

Изменить renderWaypointsList():

async function renderWaypointsList() {
  const list = document.getElementById('waypoints-list');
  if (!routeWaypoints.length) { list.innerHTML = ''; return; }

  list.innerHTML = routeWaypoints.map((wp, i) => {
    const isStart = i === 0;
    const isEnd = i === routeWaypoints.length - 1;
    const label = isStart ? 'S' : isEnd ? 'F' : String(i);
    const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff';
    const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
    return `<div class="wl-item" id="wl-item-${i}">
      <div class="wl-pin">${waypointPinSvg(label, color)}</div>
      <span class="wl-label" id="wl-label-${i}">${coordText}</span>
      <button class="wl-remove" onclick="removeWaypoint(${i})" title="Удалить">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
      </button>
    </div>`;
  }).join('');

  // Async geocode
  routeWaypoints.forEach(async (wp, i) => {
    const name = await reverseGeocode(wp.lat, wp.lon);
    const el = document.getElementById(`wl-label-${i}`);
    if (el) el.textContent = name;
  });
}

CSS для списка точек (обновить):

.wl-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}
.wl-item:last-child { border-bottom: none; }
.wl-pin { flex-shrink: 0; display: flex; align-items: center; }
.wl-label {
  flex: 1; font-size: 13px; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
}
.wl-remove {
  width: 28px; height: 28px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--text3);
  cursor: pointer; border-radius: 6px;
  -webkit-tap-highlight-color: transparent;
}
.wl-remove:active { background: var(--red-bg); color: var(--red); }
.wl-remove svg { width: 14px; height: 14px; }

Часть 3: Карточки маршрутов — редизайн

Новый дизайн карточки

┌─────────────────────────────────────┐
│ ● Вариант 1          1013 км  14ч  │  ← header
│ ████████████████░░░░  82% грунт     │  ← полоска + подпись
└─────────────────────────────────────┘
  • Полоска грунт/асфальт — высота 6px, скруглённая, без отдельных pill-бейджей
  • Под полоской: «82% грунт · 18% асфальт» одной строкой мелким текстом
  • Активная карточка: оранжевый левый бордер (4px) вместо полного бордера
  • Компактнее: padding 10px 12px вместо 12px 14px
function renderRouteCards(routes) {
  const container = document.getElementById('route-cards');
  container.innerHTML = routes.map((route, i) => {
    const color = ROUTE_COLORS[i] || '#888888';
    const distKm = (route.distance_m / 1000).toFixed(0);
    const timeStr = formatDuration(route.duration_s);
    const isActive = i === activeRouteIdx;
    const s = route.stats || {};
    const dirtPct = s.dirt_total_pct || 0;
    const asphPct = s.asphalt_pct || 0;

    return `<div class="route-card${isActive ? ' active' : ''}" onclick="selectRoute(${i})">
      <div class="rc-header">
        <span class="rc-dot" style="background:${color}"></span>
        <span class="rc-title">Вариант ${i + 1}</span>
        <span class="rc-meta">${distKm} км · ${timeStr}</span>
      </div>
      <div class="rc-bar-wrap">
        <div class="rc-bar">
          <div class="rc-bar-dirt" style="width:${dirtPct}%"></div>
          <div class="rc-bar-asphalt" style="width:${asphPct}%"></div>
        </div>
      </div>
      <div class="rc-bar-label">${dirtPct}% грунт${asphPct ? ` · ${asphPct}% асфальт` : ''}</div>
    </div>`;
  }).join('');
}

CSS карточек (обновить/добавить):

.route-card {
  background: var(--surface2);
  border: 1.5px solid var(--border);
  border-left: 4px solid transparent;
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 6px;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.route-card:active { background: var(--surface3, var(--surface2)); }
.route-card.active {
  border-color: var(--border);
  border-left-color: var(--accent);
  background: var(--accent-bg);
}
.rc-header {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 8px;
}
.rc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.rc-title { font-size: 13px; font-weight: 700; color: var(--text); flex: 1; }
.rc-meta { font-size: 12px; color: var(--text2); white-space: nowrap; }
.rc-bar-wrap { margin-bottom: 4px; }
.rc-bar {
  height: 6px; border-radius: 3px;
  background: var(--border);
  display: flex; overflow: hidden;
}
.rc-bar-dirt { background: var(--gold); border-radius: 3px 0 0 3px; }
.rc-bar-asphalt { background: var(--text3); }
.rc-bar-label { font-size: 11px; color: var(--text2); }

Часть 4: Кнопки действий — компактнее

Текущие кнопки «+ Точка», «GPX», «Сброс» — сделать компактнее, иконки без текста на мобиле (или иконка + короткий текст).

<div class="route-actions" id="route-actions" style="display:none">
  <button class="btn-action" onclick="addWaypointMode()" title="Добавить точку">
    <svg><!-- plus --></svg>
    <span>Точка</span>
  </button>
  <button class="btn-action primary" onclick="downloadGPX()" title="Скачать GPX">
    <svg><!-- download --></svg>
    <span>GPX</span>
  </button>
  <button class="btn-action danger" onclick="clearRoute()" title="Сбросить маршрут">
    <svg><!-- trash --></svg>
    <span>Сброс</span>
  </button>
</div>

CSS:

.route-actions {
  display: flex; gap: 6px; margin: 8px 0;
}
.btn-action {
  flex: 1; height: 36px;
  display: flex; align-items: center; justify-content: center; gap: 5px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 10px; color: var(--text2);
  font-size: 12px; font-weight: 600; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s, color 0.15s;
}
.btn-action svg { width: 14px; height: 14px; flex-shrink: 0; }
.btn-action:active { background: var(--surface3, var(--border)); }
.btn-action.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-action.primary:active { opacity: 0.85; }
.btn-action.danger:active { background: var(--red-bg); color: var(--red); border-color: var(--red); }

Порядок реализации

  1. Часть 1: новые SVG-маркеры (createWaypointMarkerEl)
  2. Часть 2: waypointPinSvg + renderWaypointsList + CSS
  3. Часть 3: renderRouteCards + CSS карточек
  4. Часть 4: CSS кнопок действий
  5. Деплой + проверка

Проверка

  1. Поставить старт → зелёный пин «S» на карте
  2. Поставить финиш → красный пин «F»
  3. Добавить промежуточную → синий пин «1»
  4. В списке точек: иконки пинов + названия мест
  5. Карточки маршрутов: компактные, полоска, левый оранжевый бордер у активной