SHiNE-server/shine-UI/js/pages/network/lab.js
Pixel e0f0726e68 Связи: интерактивная карта связей (force-directed graph)
Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами.

Движок (js/pages/network/force-graph.js):
- diffing-переходы: общие узлы перелетают, новые расцветают каскадом, исчезнувшие — Ghost-слой (800мс, на месте);
- мягкая радиальная пружина + отталкивание (органичная орбита), упругий влёт фокуса;
- динамическая вязкость на старте (трение 0.92→0.82, отталкивание ослаблено) — мягкий разлёт без тряски;
- жёсткая заморозка (kill-switch) при затухании — нет «треска», экономия батареи;
- линии — SVG <path> Безье (изогнутые нити), прорастание; жесты pan с инерцией;
- хард-лимит DOM-аватарок (остальное — SVG-точки).

Интеграция и UX:
- adapter.js: getUserConnectionsGraph → модель движка (сервер не трогаем, read-only);
- фильтры (Все/Семья/Друзья/Сияющие), контекстное меню (node-menu.js), нижний сниппет, профиль;
- прицел в центре, дыхание фокуса, свечение сияющих;
- лаборатория network-view/lab на мок-данных (networkGraphUsers) для тестов без бэкенда.

Документация: shine-UI/Dev_Docs/features/interactive-network-graph.md.
Бамп client.version 1.2.135 -> 1.2.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:16 +03:00

87 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Лабораторный режим карты связей (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';
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));
stage.append(header);
screen.append(stage);
// Монтируем движок синхронно (не через rAF): размеры сцены движок берёт с запасом
// (window.innerWidth) и корректирует через ResizeObserver, когда сцена попадёт в DOM.
const graph = createForceGraph({
stage,
model,
// тап по узлу — переключаем карту на сеть выбранного человека
onNodeTap: (node) => { graph.setModel(buildModelFromTz(graphForLogin(node.login))); },
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}`) },
],
}),
});
screen.cleanup = () => {
graph.destroy();
appScreenEl?.classList.remove('network-scroll-lock');
};
return screen;
}