231 lines
8.5 KiB
Markdown
231 lines
8.5 KiB
Markdown
# Dev Task: Enduro Trails — Мини-бар маршрута
|
||
|
||
**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/`
|
||
**Деплой:** `/tmp/deploy_static.js`
|
||
|
||
---
|
||
|
||
## Три состояния sheet-route
|
||
|
||
| Состояние | Как попасть | Что происходит |
|
||
|-----------|-------------|----------------|
|
||
| **Полная панель** | Тап «Маршрут» в toolbar | Открыта sheet-route |
|
||
| **Мини-бар** | Свайп вниз по handle полной панели | Панель свёрнута, маршрут на карте остаётся |
|
||
| **Закрыто** | Крестик (X) в полной панели | Панель закрыта + clearRoute() |
|
||
|
||
Мини-бар → тап или свайп вверх → полная панель.
|
||
Мини-бар → свайп влево/вправо → переключение вариантов маршрута.
|
||
|
||
---
|
||
|
||
## HTML — добавить в index.html перед `</body>`
|
||
|
||
```html
|
||
<div id="sheet-route-mini">
|
||
<div class="mini-handle" id="mini-route-handle"></div>
|
||
<div class="mini-route-info">
|
||
<div class="mini-route-dot" id="mini-dot"></div>
|
||
<div class="mini-route-text">
|
||
<div class="mini-route-label" id="mini-label">Вариант 1</div>
|
||
<div class="mini-route-stats" id="mini-stats">— км · —% грунт</div>
|
||
</div>
|
||
<div class="mini-route-arrows">
|
||
<span class="mini-arrow" id="mini-prev">‹</span>
|
||
<span class="mini-arrow" id="mini-next">›</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## CSS — добавить в app.css
|
||
|
||
```css
|
||
/* ── Mini Route Bar ───────────────────────── */
|
||
#sheet-route-mini {
|
||
position: fixed;
|
||
bottom: 72px; left: 0; right: 0;
|
||
height: 64px;
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border);
|
||
border-radius: 14px 14px 0 0;
|
||
z-index: 350;
|
||
display: none;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-shadow: 0 -4px 16px var(--shadow);
|
||
}
|
||
#sheet-route-mini.visible { display: flex; }
|
||
#sheet-route-mini .mini-handle {
|
||
width: 32px; height: 4px;
|
||
background: var(--border2, var(--border));
|
||
border-radius: 2px;
|
||
margin: 7px auto 0;
|
||
flex-shrink: 0;
|
||
}
|
||
.mini-route-info {
|
||
display: flex; align-items: center;
|
||
gap: 10px; padding: 0 16px;
|
||
flex: 1; width: 100%;
|
||
}
|
||
.mini-route-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||
.mini-route-text { flex: 1; min-width: 0; }
|
||
.mini-route-label { font-size: 13px; font-weight: 700; color: var(--text); }
|
||
.mini-route-stats { font-size: 11px; color: var(--text2); }
|
||
.mini-route-arrows { display: flex; gap: 4px; flex-shrink: 0; }
|
||
.mini-arrow {
|
||
width: 28px; height: 28px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: var(--surface2); border: 1px solid var(--border);
|
||
border-radius: 8px; font-size: 18px; color: var(--text2);
|
||
cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent;
|
||
}
|
||
.mini-arrow:active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||
|
||
@media (min-width: 768px) {
|
||
#sheet-route-mini { left: 72px; width: 380px; right: auto; border-radius: 0 14px 0 0; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## JS — добавить функции в app.js
|
||
|
||
```js
|
||
// ─── Mini Route Bar ────────────────────────────────────────────────
|
||
const ROUTE_COLORS = ['#0066ff','#ff6600','#00aa44','#aa00ff','#ff0044'];
|
||
|
||
function showMiniRouteSheet() {
|
||
if (!routeResults || routeResults.length === 0) return;
|
||
updateMiniRouteCard();
|
||
document.getElementById('sheet-route-mini').classList.add('visible');
|
||
initMiniRouteInteraction();
|
||
}
|
||
|
||
function hideMiniRouteSheet() {
|
||
const el = document.getElementById('sheet-route-mini');
|
||
if (el) el.classList.remove('visible');
|
||
}
|
||
|
||
function updateMiniRouteCard() {
|
||
const r = routeResults[activeRouteIdx];
|
||
if (!r) return;
|
||
const km = (r.distance_m / 1000).toFixed(0);
|
||
const dirt = r.stats?.dirt_total_pct ?? '?';
|
||
document.getElementById('mini-dot').style.background = ROUTE_COLORS[activeRouteIdx % ROUTE_COLORS.length];
|
||
document.getElementById('mini-label').textContent = `Вариант ${activeRouteIdx + 1} из ${routeResults.length}`;
|
||
document.getElementById('mini-stats').textContent = `${km} км · ${dirt}% грунт`;
|
||
document.getElementById('mini-prev').style.opacity = activeRouteIdx > 0 ? '1' : '0.3';
|
||
document.getElementById('mini-next').style.opacity = activeRouteIdx < routeResults.length - 1 ? '1' : '0.3';
|
||
}
|
||
|
||
function selectMiniRoute(idx) {
|
||
if (idx < 0 || idx >= routeResults.length) return;
|
||
activeRouteIdx = idx;
|
||
const map = window._map;
|
||
for (let i = 0; i < routeResults.length; i++) {
|
||
const op = i === idx ? 1 : 0.35;
|
||
if (map.getLayer('route-line-' + i)) map.setPaintProperty('route-line-' + i, 'line-opacity', op);
|
||
if (map.getLayer('route-line-' + i + '-outline')) map.setPaintProperty('route-line-' + i + '-outline', 'line-opacity', op);
|
||
}
|
||
updateMiniRouteCard();
|
||
}
|
||
|
||
function initMiniRouteInteraction() {
|
||
const mini = document.getElementById('sheet-route-mini');
|
||
if (!mini) return;
|
||
|
||
// Re-bind arrow buttons
|
||
document.getElementById('mini-prev').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx - 1); };
|
||
document.getElementById('mini-next').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx + 1); };
|
||
|
||
// Remove old touch listeners by replacing element
|
||
const newMini = mini.cloneNode(true);
|
||
mini.parentNode.replaceChild(newMini, mini);
|
||
document.getElementById('mini-prev').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx - 1); };
|
||
document.getElementById('mini-next').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx + 1); };
|
||
|
||
let startX = 0, startY = 0;
|
||
newMini.addEventListener('touchstart', e => {
|
||
startX = e.touches[0].clientX;
|
||
startY = e.touches[0].clientY;
|
||
}, { passive: true });
|
||
|
||
newMini.addEventListener('touchend', e => {
|
||
const dx = e.changedTouches[0].clientX - startX;
|
||
const dy = e.changedTouches[0].clientY - startY;
|
||
if (Math.abs(dy) > Math.abs(dx)) {
|
||
if (dy < -40) { hideMiniRouteSheet(); openSheet('sheet-route'); }
|
||
} else {
|
||
if (dx < -40) selectMiniRoute(activeRouteIdx + 1);
|
||
if (dx > 40) selectMiniRoute(activeRouteIdx - 1);
|
||
}
|
||
});
|
||
|
||
newMini.addEventListener('click', e => {
|
||
if (e.target.classList.contains('mini-arrow')) return;
|
||
hideMiniRouteSheet();
|
||
openSheet('sheet-route');
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Изменить существующие функции
|
||
|
||
### minimizeSheet(id)
|
||
```js
|
||
function minimizeSheet(id) {
|
||
closeSheet(id);
|
||
if (id === 'sheet-route' && routeResults && routeResults.length > 0) {
|
||
showMiniRouteSheet();
|
||
}
|
||
}
|
||
```
|
||
|
||
### clearRoute() — в самое начало добавить:
|
||
```js
|
||
hideMiniRouteSheet();
|
||
```
|
||
|
||
### toggleRouteMode() — при входе в режим добавить перед openSheet:
|
||
```js
|
||
hideMiniRouteSheet();
|
||
```
|
||
|
||
### initSheetSwipe() — найти обработчик touchend и изменить логику закрытия:
|
||
Найти строку вида `if (dy > 80) closeSheet(sheet.id)` или `if (dy > 80) { closeSheet(...) }` и заменить на:
|
||
```js
|
||
if (dy > 80) {
|
||
if (sheet.id === 'sheet-route' && routeResults && routeResults.length > 0) {
|
||
minimizeSheet(sheet.id);
|
||
} else {
|
||
closeSheet(sheet.id);
|
||
}
|
||
sheet.style.transform = '';
|
||
}
|
||
```
|
||
|
||
### Крестик (X) в sheet-route — уже вызывает minimizeSheet('sheet-route').
|
||
Нужно изменить на toggleRouteMode() чтобы крестик стирал маршрут:
|
||
В index.html найти:
|
||
```html
|
||
<button class="sheet-close" onclick="minimizeSheet('sheet-route')">
|
||
```
|
||
Заменить на:
|
||
```html
|
||
<button class="sheet-close" onclick="toggleRouteMode()">
|
||
```
|
||
(toggleRouteMode при routeMode=true → выходит из режима + clearRoute)
|
||
|
||
---
|
||
|
||
## Проверка после деплоя
|
||
1. Построить маршрут → свайп вниз по handle → появляется мини-бар
|
||
2. Свайп влево/вправо на мини-баре → переключение вариантов (если >1)
|
||
3. Тап на мини-бар → раскрывается полная панель
|
||
4. Крестик в полной панели → всё закрывается, маршрут стирается с карты
|
||
5. Мини-бар не появляется если маршрут не построен
|