Связи: лаборатория только локально (исключена из репозитория)
- shine-UI/js/pages/network/lab.js убран из git (git rm --cached) и добавлен в .gitignore: на сервере лаборатория не нужна (там реальные данные), локально файл остаётся и работает. - network-view.js: статический импорт lab.js заменён на ДИНАМИЧЕСКИЙ с фолбэком — если файла нет (прод/сервер), реальный экран «Связи» не ломается, а заход на /network-view/lab уводит на обычный экран. Локально лаборатория грузится как прежде. - Документация фичи отражает, что lab.js — локальный (в .gitignore). - Бамп client.version → 1.2.140. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e6e96c4b0d
commit
369ef61cab
4
.gitignore
vendored
4
.gitignore
vendored
@ -93,3 +93,7 @@ ESP32/**/*.a
|
|||||||
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
||||||
server-backup/archive/**
|
server-backup/archive/**
|
||||||
!server-backup/archive/.gitkeep
|
!server-backup/archive/.gitkeep
|
||||||
|
|
||||||
|
# Лаборатория карты связей — только для локальной разработки (на сервере не нужна).
|
||||||
|
# Файл остаётся локально и работает; network-view.js грузит его динамически (с фолбэком).
|
||||||
|
shine-UI/js/pages/network/lab.js
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.139
|
client.version=1.2.140
|
||||||
server.version=1.2.127
|
server.version=1.2.127
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
- `js/pages/network/adapter.js` — реальные данные → нейтральная модель движка.
|
- `js/pages/network/adapter.js` — реальные данные → нейтральная модель движка.
|
||||||
- `js/pages/network/node-menu.js` — общее контекстное меню узла.
|
- `js/pages/network/node-menu.js` — общее контекстное меню узла.
|
||||||
- `js/pages/network/lab.js` — лаборатория (`network-view/lab`) на мок-данных, без бэкенда.
|
- `js/pages/network/lab.js` — лаборатория (`network-view/lab`) на мок-данных, без бэкенда.
|
||||||
|
**Только локально** (в `.gitignore`, в репозитории её нет): на сервере она не нужна. `network-view.js`
|
||||||
|
грузит её **динамическим** импортом — если файла нет (прод), реальный экран «Связи» не ломается.
|
||||||
- `js/pages/network-view.js` — страница: шапка, поиск, фильтры, история, склейка с движком.
|
- `js/pages/network-view.js` — страница: шапка, поиск, фильтры, история, склейка с движком.
|
||||||
- `js/mock-data.js` — `networkGraphUsers` (связанный мульти-граф из 10 человек для лаборатории).
|
- `js/mock-data.js` — `networkGraphUsers` (связанный мульти-граф из 10 человек для лаборатории).
|
||||||
- `styles/network-graph.css` — все стили `.fg-*`.
|
- `styles/network-graph.css` — все стили `.fg-*`.
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { renderHeader } from '../components/header.js';
|
|||||||
import { authService, state } from '../state.js';
|
import { authService, state } from '../state.js';
|
||||||
import { makeProfileRoute } from '../services/shine-routes.js';
|
import { makeProfileRoute } from '../services/shine-routes.js';
|
||||||
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
||||||
import { renderNetworkLab } from './network/lab.js';
|
|
||||||
import { createForceGraph } from './network/force-graph.js';
|
import { createForceGraph } from './network/force-graph.js';
|
||||||
import { engineModelFromGraphModel } from './network/adapter.js';
|
import { engineModelFromGraphModel } from './network/adapter.js';
|
||||||
import { openNodeMenu, relationLabelRu } from './network/node-menu.js';
|
import { openNodeMenu, relationLabelRu } from './network/node-menu.js';
|
||||||
@ -217,9 +216,19 @@ let persistedCenterLogin = '';
|
|||||||
let persistedCenterHistory = [];
|
let persistedCenterHistory = [];
|
||||||
|
|
||||||
export function render({ navigate, route }) {
|
export function render({ navigate, route }) {
|
||||||
// Лабораторный режим force-графа (Фаза 1): рендерится из мока, не трогая реальный путь ниже.
|
// Лабораторный режим force-графа — ТОЛЬКО для локальной разработки. Файл `network/lab.js`
|
||||||
|
// намеренно НЕ в гите (см. .gitignore): на сервере он не нужен (там реальные данные). Поэтому
|
||||||
|
// импорт ДИНАМИЧЕСКИЙ — если файла нет (прод/сервер), реальный экран «Связи» не ломается,
|
||||||
|
// а заход на /network-view/lab просто уводит на обычный экран.
|
||||||
if (String(route?.params?.mode || '').trim().toLowerCase() === 'lab') {
|
if (String(route?.params?.mode || '').trim().toLowerCase() === 'lab') {
|
||||||
return renderNetworkLab({ navigate });
|
const host = document.createElement('div');
|
||||||
|
host.style.display = 'contents'; // прозрачная обёртка: lab-экран ведёт себя как прямой потомок
|
||||||
|
let inner = null;
|
||||||
|
import('./network/lab.js')
|
||||||
|
.then((m) => { inner = m.renderNetworkLab({ navigate }); host.append(inner); })
|
||||||
|
.catch(() => { navigate('network-view'); }); // нет файла (сервер) — на реальный экран
|
||||||
|
host.cleanup = () => { if (inner && typeof inner.cleanup === 'function') inner.cleanup(); };
|
||||||
|
return host;
|
||||||
}
|
}
|
||||||
const keepHistory = String(route?.params?.mode || '').trim().toLowerCase() === 'keep-history';
|
const keepHistory = String(route?.params?.mode || '').trim().toLowerCase() === 'keep-history';
|
||||||
const routeLogin = normalizeLogin(route?.params?.login || '');
|
const routeLogin = normalizeLogin(route?.params?.login || '');
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
// Лабораторный режим карты связей (network-view/lab).
|
|
||||||
//
|
|
||||||
// Назначение: на мок-данных (без бэкенда) щупать физику force-графа, панорамирование,
|
|
||||||
// центрирование и навигацию между пользователями. Используется связанный мульти-граф
|
|
||||||
// networkGraphUsers: у каждого пользователя свой набор связей, тап по узлу переключает
|
|
||||||
// карту на сеть этого человека (как реальный путь, но локально).
|
|
||||||
|
|
||||||
import { renderHeader } from '../../components/header.js';
|
|
||||||
import { networkGraphUsers } from '../../mock-data.js';
|
|
||||||
import { createForceGraph, buildModelFromTz } from './force-graph.js';
|
|
||||||
import { openNodeMenu } from './node-menu.js';
|
|
||||||
|
|
||||||
const START_LOGIN = 'ivan';
|
|
||||||
|
|
||||||
// Фильтры слоёв — те же, что в реальном пути network-view (предикат по периферийным узлам;
|
|
||||||
// фокус виден всегда). Позволяют пощупать в лаборатории в т.ч. слой «Сияющие».
|
|
||||||
const FILTERS = {
|
|
||||||
all: { label: 'Все', pred: () => true },
|
|
||||||
family: { label: 'Семья', pred: (n) => n.relationType === 'family' },
|
|
||||||
friends: { label: 'Друзья', pred: (n) => n.relationType === 'friend' },
|
|
||||||
shining: { label: 'Сияющие', pred: (n) => Boolean(n.shining) },
|
|
||||||
};
|
|
||||||
const FILTER_ORDER = ['all', 'family', 'friends', 'shining'];
|
|
||||||
|
|
||||||
function helpText() {
|
|
||||||
return [
|
|
||||||
'Лаборатория карты связей (мок-данные, без сервера).',
|
|
||||||
'• Тащите по экрану — карта свободно перемещается (pan).',
|
|
||||||
'• Тап по узлу — он стягивается в центр и карта перестраивается под его сеть.',
|
|
||||||
'• Тап по центральному узлу — здесь открылся бы профиль.',
|
|
||||||
'• Долгое нажатие — контекстное меню (в реальном пути «Связи»).',
|
|
||||||
'• Чипы под шапкой — фильтры слоёв: Все / Семья / Друзья / Сияющие.',
|
|
||||||
'',
|
|
||||||
'Доступные люди: Иван, Алиса, Павел, Елена, Дмитрий, Олег, Нина, Марина, Света, Кирилл.',
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Граф пользователя по логину; если такого нет в датасете — одинокий узел (без связей).
|
|
||||||
function graphForLogin(login) {
|
|
||||||
const key = String(login || '').trim().toLowerCase();
|
|
||||||
if (networkGraphUsers[key]) return networkGraphUsers[key];
|
|
||||||
return { focusUser: { id: key, login: key, name: key, avatar: null, status: '' }, connections: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderNetworkLab({ navigate }) {
|
|
||||||
const screen = document.createElement('section');
|
|
||||||
screen.className = 'network-screen';
|
|
||||||
|
|
||||||
const appScreenEl = document.getElementById('app-screen');
|
|
||||||
appScreenEl?.classList.add('network-scroll-lock');
|
|
||||||
|
|
||||||
const stage = document.createElement('div');
|
|
||||||
stage.className = 'network-stage fg-stage';
|
|
||||||
|
|
||||||
const header = renderHeader({
|
|
||||||
title: 'Связи · лаборатория',
|
|
||||||
leftAction: {
|
|
||||||
label: '←',
|
|
||||||
onClick: () => navigate('network-view'),
|
|
||||||
},
|
|
||||||
rightActions: [
|
|
||||||
{ label: '?', onClick: () => window.alert(helpText()) },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
header.classList.add('network-header-overlay');
|
|
||||||
|
|
||||||
const model = buildModelFromTz(graphForLogin(START_LOGIN));
|
|
||||||
|
|
||||||
// Состояние активного слоя (как в network-view): фокус всегда виден.
|
|
||||||
let activeFilter = 'all';
|
|
||||||
const filterChips = {};
|
|
||||||
function applyFilter(key) {
|
|
||||||
if (!FILTERS[key]) return;
|
|
||||||
activeFilter = key;
|
|
||||||
FILTER_ORDER.forEach((k) => {
|
|
||||||
const el = filterChips[k];
|
|
||||||
if (el) el.classList.toggle('is-active', k === activeFilter);
|
|
||||||
});
|
|
||||||
graph.setFilter(FILTERS[key].pred);
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.append(header);
|
|
||||||
screen.append(stage);
|
|
||||||
|
|
||||||
// Монтируем движок синхронно (не через rAF): размеры сцены движок берёт с запасом
|
|
||||||
// (window.innerWidth) и корректирует через ResizeObserver, когда сцена попадёт в DOM.
|
|
||||||
const graph = createForceGraph({
|
|
||||||
stage,
|
|
||||||
model,
|
|
||||||
// тап по узлу — переключаем карту на сеть выбранного человека
|
|
||||||
onNodeTap: (node) => {
|
|
||||||
graph.setModel(buildModelFromTz(graphForLogin(node.login)));
|
|
||||||
// сохраняем выбранный слой при переходе на сеть другого человека (как в network-view)
|
|
||||||
if (activeFilter !== 'all') graph.setFilter(FILTERS[activeFilter].pred);
|
|
||||||
},
|
|
||||||
onCenterTap: (node) => window.alert(`Профиль: ${node.name || node.login || node.id}`),
|
|
||||||
onNodeLongPress: (node, point) => openNodeMenu({
|
|
||||||
login: node.name || node.login || node.id,
|
|
||||||
relationType: node.relationType,
|
|
||||||
point,
|
|
||||||
actions: [
|
|
||||||
{ label: 'Профиль', onClick: () => window.alert(`Профиль: ${node.name || node.login}`) },
|
|
||||||
{ label: 'Написать', onClick: () => window.alert(`Написать: ${node.name || node.login}`) },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Панель фильтров слоёв — тот же оверлей-стиль (.fg-filter-bar), что и в network-view.
|
|
||||||
const filterBar = document.createElement('div');
|
|
||||||
filterBar.className = 'fg-filter-bar';
|
|
||||||
// Не даём нажатию на чип «провалиться» в сцену: иначе движок делает setPointerCapture на stage,
|
|
||||||
// а захват указателя перенаправляет нативный click со сцены — и кнопка фильтра не срабатывает.
|
|
||||||
filterBar.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
||||||
FILTER_ORDER.forEach((key) => {
|
|
||||||
const chip = document.createElement('button');
|
|
||||||
chip.type = 'button';
|
|
||||||
chip.className = `fg-filter-chip${key === activeFilter ? ' is-active' : ''}`;
|
|
||||||
chip.textContent = FILTERS[key].label;
|
|
||||||
chip.addEventListener('click', () => applyFilter(key));
|
|
||||||
filterChips[key] = chip;
|
|
||||||
filterBar.append(chip);
|
|
||||||
});
|
|
||||||
stage.append(filterBar);
|
|
||||||
|
|
||||||
screen.cleanup = () => {
|
|
||||||
graph.destroy();
|
|
||||||
appScreenEl?.classList.remove('network-scroll-lock');
|
|
||||||
};
|
|
||||||
|
|
||||||
return screen;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user