auto-sync: 2026-05-05 17:10:02
This commit is contained in:
166
tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md
Normal file
166
tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Dev Task: Enduro Trails — UX фиксы #1, #3, #5
|
||||
|
||||
**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/`
|
||||
**Деплой:** `node /tmp/deploy_static.js`
|
||||
**Бэкенд не трогать.**
|
||||
|
||||
---
|
||||
|
||||
## Фикс 1: Кнопка «+» на мини-баре
|
||||
|
||||
Сейчас чтобы добавить промежуточную точку нужно открыть полную панель.
|
||||
Нужно: добавить кнопку «+» прямо на мини-баре.
|
||||
|
||||
### HTML — в `#sheet-route-mini` добавить кнопку рядом со стрелками:
|
||||
```html
|
||||
<button class="mini-add-btn" id="mini-add-btn" onclick="miniAddWaypoint()" title="Добавить точку">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
```
|
||||
|
||||
Вставить в `.mini-route-info` после `.mini-route-arrows`.
|
||||
|
||||
### CSS:
|
||||
```css
|
||||
.mini-add-btn {
|
||||
width: 32px; height: 32px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--accent); border: none;
|
||||
border-radius: 10px; color: #fff;
|
||||
cursor: pointer; flex-shrink: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.mini-add-btn:active { opacity: 0.8; transform: scale(0.94); }
|
||||
```
|
||||
|
||||
### JS — новая функция:
|
||||
```js
|
||||
function miniAddWaypoint() {
|
||||
// Enter waypoint-adding mode without opening full sheet
|
||||
if (!routeMode) {
|
||||
// Re-enter route mode silently (no sheet open)
|
||||
routeMode = true;
|
||||
document.getElementById('tb-route').classList.add('active');
|
||||
updateMapModeClass();
|
||||
}
|
||||
addWaypointMode();
|
||||
// Show hint on mini-bar
|
||||
document.getElementById('mini-stats').textContent = 'Тапни на карте для добавления точки';
|
||||
}
|
||||
```
|
||||
|
||||
После добавления точки (в обработчике клика карты где `addingWaypoint = false`) — восстановить нормальный текст мини-бара:
|
||||
```js
|
||||
// После rebuildWaypointMarkers(); renderWaypointsList(); при addingWaypoint
|
||||
updateMiniRouteCard(); // обновить мини-бар
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Фикс 3: После добавления точки — свернуть панель в мини-бар
|
||||
|
||||
Сейчас после добавления точки полная панель остаётся открытой.
|
||||
Нужно: после успешного построения маршрута (когда routeResults обновились) — если панель открыта, свернуть её в мини-бар.
|
||||
|
||||
### В функции `drawRouteResults()` — в конце добавить:
|
||||
```js
|
||||
// Auto-minimize sheet after route is built
|
||||
const sheet = document.getElementById('sheet-route');
|
||||
if (sheet && sheet.classList.contains('open')) {
|
||||
minimizeSheet('sheet-route');
|
||||
}
|
||||
```
|
||||
|
||||
**Важно:** это должно срабатывать только когда маршрут уже был (перестройка), а не при первом построении. Проверить: если `routeResults.length > 0` до вызова — значит это перестройка, сворачивать. Если первый раз — не сворачивать (пусть пользователь видит варианты).
|
||||
|
||||
Логика:
|
||||
```js
|
||||
const wasBuilt = routeResults.length > 0; // до обновления routeResults
|
||||
// ... существующий код drawRouteResults ...
|
||||
// В конце:
|
||||
if (wasBuilt) {
|
||||
const sheet = document.getElementById('sheet-route');
|
||||
if (sheet && sheet.classList.contains('open')) {
|
||||
minimizeSheet('sheet-route');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Фикс 5: Reverse geocoding — названия мест вместо координат
|
||||
|
||||
Сейчас в списке точек показываются координаты: `55.7234, 37.6123`.
|
||||
Нужно: показывать название места через Nominatim reverse geocoding.
|
||||
|
||||
### Добавить функцию reverse geocoding:
|
||||
```js
|
||||
const geocodeCache = {};
|
||||
|
||||
async function reverseGeocode(lat, lon) {
|
||||
const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
|
||||
if (geocodeCache[key]) return geocodeCache[key];
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=ru`,
|
||||
{ headers: { 'Accept-Language': 'ru' } }
|
||||
);
|
||||
const data = await resp.json();
|
||||
// Приоритет: village/town/city > road > county
|
||||
const a = data.address || {};
|
||||
const name = a.village || a.town || a.city || a.suburb || a.road || a.county || a.state || `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
|
||||
geocodeCache[key] = name;
|
||||
return name;
|
||||
} catch(e) {
|
||||
return `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Изменить `renderWaypointsList()`:
|
||||
|
||||
Сделать функцию async и использовать geocoding:
|
||||
```js
|
||||
async function renderWaypointsList() {
|
||||
const list = document.getElementById('waypoints-list');
|
||||
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||
|
||||
// Render immediately with coords, then update with names
|
||||
list.innerHTML = routeWaypoints.map((wp, i) => {
|
||||
const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid';
|
||||
const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff';
|
||||
const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
|
||||
return `<div class="wl-item" id="wl-item-${i}">
|
||||
<div class="wl-dot" style="background:${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" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Async update with place names
|
||||
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;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Также обновить мини-бар stats при первом построении маршрута:
|
||||
В `updateMiniRouteCard()` — stats показывает км и % грунт, это ок. Не трогать.
|
||||
|
||||
---
|
||||
|
||||
## Порядок реализации
|
||||
1. Фикс 5 (geocoding) — добавить функцию, изменить renderWaypointsList
|
||||
2. Фикс 3 (auto-minimize) — изменить drawRouteResults
|
||||
3. Фикс 1 (кнопка + на мини-баре) — HTML + CSS + JS
|
||||
4. Деплой + проверка
|
||||
|
||||
## Проверка
|
||||
1. Построить маршрут A→B → панель показывает варианты (не сворачивается при первом построении)
|
||||
2. Добавить промежуточную точку через кнопку «+» в мини-баре → маршрут перестраивается → панель сворачивается
|
||||
3. Открыть полную панель → в списке точек видны названия мест (не координаты)
|
||||
4. Добавить точку через кнопку «+ Точка» в полной панели → маршрут перестраивается → панель сворачивается
|
||||
68
tasks/enduro-trails/prototype/static/app.js
vendored
68
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -497,22 +497,51 @@ function rebuildWaypointMarkers() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderWaypointsList() {
|
||||
// ─── Reverse Geocoding ───────────────────────────────────────────
|
||||
const geocodeCache = {};
|
||||
|
||||
async function reverseGeocode(lat, lon) {
|
||||
const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
|
||||
if (geocodeCache[key]) return geocodeCache[key];
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=ru`,
|
||||
{ headers: { 'Accept-Language': 'ru' } }
|
||||
);
|
||||
const data = await resp.json();
|
||||
const a = data.address || {};
|
||||
const name = a.village || a.town || a.city || a.suburb || a.road || a.county || a.state || `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
|
||||
geocodeCache[key] = name;
|
||||
return name;
|
||||
} catch(e) {
|
||||
return `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderWaypointsList() {
|
||||
const list = document.getElementById('waypoints-list');
|
||||
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||
|
||||
// Render immediately with coords, then update with place names
|
||||
list.innerHTML = routeWaypoints.map((wp, i) => {
|
||||
let labelClass, labelText;
|
||||
if (i === 0) { labelClass = 'start'; labelText = 'A'; }
|
||||
else if (i === routeWaypoints.length - 1) { labelClass = 'end'; labelText = 'B'; }
|
||||
else { labelClass = 'mid'; labelText = String(i); }
|
||||
return `<div class="wl-item">
|
||||
<div class="wl-dot" style="background:${labelClass==='start'?'var(--success)':labelClass==='end'?'var(--red)':'#0066ff'}"></div>
|
||||
<span class="wl-label">${wp.lat.toFixed(4)}, ${wp.lon.toFixed(4)}</span>
|
||||
const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid';
|
||||
const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff';
|
||||
const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
|
||||
return `<div class="wl-item" id="wl-item-${i}">
|
||||
<div class="wl-dot" style="background:${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" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Async update labels with place names
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
function removeWaypoint(idx) {
|
||||
@@ -576,6 +605,7 @@ async function buildRoute() {
|
||||
function drawRouteResults(routes, activeIdx) {
|
||||
const map = window._map;
|
||||
activeRouteIdx = activeIdx;
|
||||
const wasBuilt = routeResults.length > 0; // track rebuild vs first build
|
||||
routeResults = routes;
|
||||
|
||||
// Clear old layers
|
||||
@@ -630,6 +660,14 @@ function drawRouteResults(routes, activeIdx) {
|
||||
// Update mini sheet if visible
|
||||
const miniEl = document.getElementById('sheet-route-mini');
|
||||
if (miniEl && miniEl.classList.contains('visible')) showMiniRouteSheet();
|
||||
|
||||
// Auto-minimize sheet on rebuild (not on first build)
|
||||
if (wasBuilt) {
|
||||
const sheet = document.getElementById('sheet-route');
|
||||
if (sheet && sheet.classList.contains('open')) {
|
||||
minimizeSheet('sheet-route');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectRoute(idx) {
|
||||
@@ -1034,6 +1072,7 @@ function initRouteClicks(map) {
|
||||
}
|
||||
rebuildWaypointMarkers(); renderWaypointsList();
|
||||
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||
updateMiniRouteCard();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1618,6 +1657,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ─── Mini Route Bar ──────────────────────────────────────────────────
|
||||
|
||||
function miniAddWaypoint() {
|
||||
// Enter waypoint-adding mode without opening full sheet
|
||||
if (!routeMode) {
|
||||
routeMode = true;
|
||||
document.getElementById('tb-route').classList.add('active');
|
||||
updateMapModeClass();
|
||||
}
|
||||
addWaypointMode();
|
||||
// Show hint on mini-bar
|
||||
const statsEl = document.getElementById('mini-stats');
|
||||
if (statsEl) statsEl.textContent = 'Тапни на карте для добавления точки';
|
||||
}
|
||||
|
||||
function showMiniRouteSheet() {
|
||||
if (!routeResults || routeResults.length === 0) return;
|
||||
updateMiniRouteCard();
|
||||
|
||||
@@ -242,6 +242,9 @@
|
||||
<span class="mini-arrow" id="mini-prev">‹</span>
|
||||
<span class="mini-arrow" id="mini-next">›</span>
|
||||
</div>
|
||||
<button class="mini-add-btn" id="mini-add-btn" onclick="miniAddWaypoint()" title="Добавить точку">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user