feat: frontend MVP — детальная схема связей устройств (AntV X6)
- React 18 + TypeScript strict + AntV X6 2.x + AntD 5 + Zustand - Custom nodes: SiteNode, CrossDeviceNode, SpliceNode, DeviceNode, CardNode - 8-слойный автолейаут, порты (left/right), линии с цветами по статусу - Toolbar, дерево навигации, карточка объекта, таблица соединений - Контекстные меню, легенда, drag линий/нод, создание линий из портов - Моковые данные: 3 сайта, 10 устройств, 15 линий Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
531
ARCHITECTURE.md
Normal file
531
ARCHITECTURE.md
Normal file
@ -0,0 +1,531 @@
|
||||
# Test-X6: Детальная схема связей устройства — Архитектура
|
||||
|
||||
## Обзор проекта
|
||||
|
||||
Веб-приложение для визуализации и редактирования детальных схем связей сетевых устройств. Основано на библиотеке **AntV X6** для рендеринга интерактивных графов. Позволяет отображать сайты, устройства, карты, порты и линии связи с полной поддержкой вложенности, автоматической раскладки и многопользовательского редактирования.
|
||||
|
||||
---
|
||||
|
||||
## Технологический стек
|
||||
|
||||
| Слой | Технологии |
|
||||
|------|-----------|
|
||||
| **Frontend** | React 18, TypeScript, AntD 5, AntV X6 2.x, Vite |
|
||||
| **Backend** | NestJS, TypeORM, PostgreSQL |
|
||||
| **Инфраструктура** | Docker, k3s, Drone CI, Harbor |
|
||||
| **Линтинг** | ESLint, Prettier, tsc strict mode |
|
||||
|
||||
---
|
||||
|
||||
## Структура монорепозитория
|
||||
|
||||
```
|
||||
test-x6/
|
||||
├── frontend/ # React-приложение
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API-клиент, типы запросов/ответов
|
||||
│ │ ├── components/ # Общие UI-компоненты
|
||||
│ │ │ ├── Layout/ # Общий layout приложения
|
||||
│ │ │ ├── Toolbar/ # Панель инструментов
|
||||
│ │ │ ├── SidePanel/ # Левая и правая боковые панели
|
||||
│ │ │ └── ConnectionsPanel/# Нижняя панель соединений
|
||||
│ │ ├── features/
|
||||
│ │ │ └── schema/ # Основной модуль схемы
|
||||
│ │ │ ├── graph/ # Инициализация и конфигурация X6 Graph
|
||||
│ │ │ ├── nodes/ # Custom Nodes (Site, Cross, Splice, Device, Card)
|
||||
│ │ │ ├── ports/ # Логика портов (контейнеры, сортировка, drag)
|
||||
│ │ │ ├── edges/ # Линии (стили, группировка, перемычки)
|
||||
│ │ │ ├── layout/ # Автолейаут (8 слоёв)
|
||||
│ │ │ ├── context-menu/# Контекстные меню
|
||||
│ │ │ ├── selection/ # Выделение (rectangle, lasso)
|
||||
│ │ │ ├── locking/ # Блокировка / concurrency
|
||||
│ │ │ └── export/ # Экспорт PNG, Excel
|
||||
│ │ ├── hooks/ # React-хуки
|
||||
│ │ ├── store/ # Zustand store
|
||||
│ │ ├── types/ # TypeScript типы и интерфейсы
|
||||
│ │ ├── utils/ # Утилиты
|
||||
│ │ ├── constants/ # Константы (цвета, размеры, слои)
|
||||
│ │ ├── App.tsx
|
||||
│ │ └── main.tsx
|
||||
│ ├── index.html
|
||||
│ ├── vite.config.ts
|
||||
│ ├── tsconfig.json
|
||||
│ ├── .eslintrc.cjs
|
||||
│ └── package.json
|
||||
├── backend/ # NestJS API
|
||||
│ ├── src/
|
||||
│ │ ├── modules/
|
||||
│ │ │ ├── sites/ # CRUD сайтов
|
||||
│ │ │ ├── devices/ # CRUD устройств (активных и пассивных)
|
||||
│ │ │ ├── cards/ # CRUD карт
|
||||
│ │ │ ├── ports/ # CRUD портов
|
||||
│ │ │ ├── lines/ # CRUD линий связи
|
||||
│ │ │ ├── schema/ # Сборка и отдача схемы, сохранение layout
|
||||
│ │ │ ├── locks/ # Блокировки редактирования
|
||||
│ │ │ └── export/ # Экспорт (PNG-метаданные, Excel)
|
||||
│ │ ├── common/ # Guards, interceptors, filters, pipes
|
||||
│ │ ├── database/ # TypeORM конфигурация, миграции
|
||||
│ │ ├── app.module.ts
|
||||
│ │ └── main.ts
|
||||
│ ├── tsconfig.json
|
||||
│ ├── .eslintrc.cjs
|
||||
│ └── package.json
|
||||
├── CLAUDE.md
|
||||
├── ARCHITECTURE.md
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Архитектура Frontend
|
||||
|
||||
### Граф (X6 Graph)
|
||||
|
||||
Центральный компонент — `SchemaCanvas`, обёртка над `@antv/x6` Graph. Инициализация:
|
||||
|
||||
```
|
||||
SchemaCanvas
|
||||
├── useGraphInit() — создание экземпляра Graph, плагины (Selection, Snapline, MiniMap, Keyboard)
|
||||
├── useGraphData() — загрузка данных, маппинг в ноды/эджи X6
|
||||
├── useGraphInteraction() — обработчики событий (click, dblclick, contextmenu, drag)
|
||||
└── useGraphLayout() — автоматическая раскладка (8 слоёв)
|
||||
```
|
||||
|
||||
### Custom Nodes
|
||||
|
||||
Каждый тип ноды — отдельный класс, наследующий `Shape.Rect` (или `Shape.HTML` для сложных случаев):
|
||||
|
||||
| Нода | Класс | Особенности |
|
||||
|------|-------|-------------|
|
||||
| **SiteNode** | `Shape.Rect` + custom markup | Контейнер с шапкой (чёрный фон, 4 строки), вложенность (embedding), динамический размер |
|
||||
| **CrossDeviceNode** | `Shape.Rect` + custom attrs | Асимметричные скругления (rx/ry по углам через `<path>`), пропорция 1:3, L/S порты |
|
||||
| **SpliceClosureNode** | `Shape.Rect` | Квадрат 98×98, border-radius 6px |
|
||||
| **OtherDeviceNode** | `Shape.Rect` | Адаптивный размер под количество портов |
|
||||
| **CardNode** | `Shape.Rect` (embedded) | Вложенный в устройство, асимметричный контур (верх/низ 1.5px, бока 5px), условное отображение |
|
||||
|
||||
### Система портов
|
||||
|
||||
```typescript
|
||||
// Двухконтейнерная архитектура портов
|
||||
interface PortContainerConfig {
|
||||
left: PortGroup; // Левый контейнер (ЛК)
|
||||
right: PortGroup; // Правый контейнер (ПК)
|
||||
}
|
||||
```
|
||||
|
||||
Логика распределения:
|
||||
1. При загрузке — определение стороны на основе относительного положения связанных устройств
|
||||
2. При перемещении устройства — `node:move` событие, пересчёт пересечения границ, автопереброс портов
|
||||
3. Ручной drag — пользователь перетаскивает порт между ЛК и ПК
|
||||
|
||||
### Линии (Edges)
|
||||
|
||||
```typescript
|
||||
// Конфигурация линии
|
||||
interface EdgeConfig {
|
||||
style: EdgeLineStyle; // solid, dashed, dotted (Appendix B)
|
||||
color: string; // по статусу (Appendix A)
|
||||
medium: 'optical' | 'copper' | 'wireless' | 'unknown';
|
||||
router: 'orth' | 'normal'; // ломаная / прямая
|
||||
grouped: boolean; // входит ли в группу
|
||||
}
|
||||
```
|
||||
|
||||
Группировка: при >1 линии между парой устройств — рендеринг одной линии с badge-счётчиком. Тултип по hover — разбивка по статусам.
|
||||
|
||||
### Автолейаут (8 слоёв)
|
||||
|
||||
```typescript
|
||||
// Маппинг типов устройств на слои
|
||||
const LAYER_MAPPING: Record<number, DeviceType[]> = {
|
||||
1: ['cross_optical', 'rrl', 'wireless', 'satellite'],
|
||||
2: ['tspu', 'dwdm'],
|
||||
3: ['men', 'sdh', 'multiservice_platform'],
|
||||
4: ['ip', 'optical_modem', 'optical_mux', 'lan_wlan'],
|
||||
5: ['ran_controller', 'mgn', 'mgx', 'server', 'sorm', 'mob', 'fix'],
|
||||
6: ['voip', 'xdsl', 'pdh'],
|
||||
7: ['ran_base_station', 'unknown', 'video_surveillance'],
|
||||
8: [], // overflow from layer 1 (>6 cross) + copper cross
|
||||
};
|
||||
```
|
||||
|
||||
Алгоритм:
|
||||
1. Определить тип каждого устройства → маппинг в слой
|
||||
2. Проверить overflow (слой 1, >6 кроссов → перенос в слой 8)
|
||||
3. Рассчитать Y-координаты слоёв (пустые — схлопнуть)
|
||||
4. Внутри слоя — расположить устройства в ряд с отступами
|
||||
5. Применить позиции
|
||||
|
||||
### State Management (Zustand)
|
||||
|
||||
```
|
||||
SchemaStore
|
||||
├── graph: Graph | null — экземпляр X6 Graph
|
||||
├── data: SchemaData — текущие данные схемы
|
||||
├── mode: 'view' | 'edit' — режим работы
|
||||
├── selectedElements: string[] — выделенные элементы
|
||||
├── clipboard: ClipboardData | null — буфер обмена
|
||||
├── displaySettings: DisplaySettings — настройки отображения
|
||||
├── locks: LockInfo[] — информация о блокировках
|
||||
├── portSettings: Record<string, PortSettings> — пользовательские настройки портов
|
||||
└── layoutPositions: Record<string, Position> — сохранённые позиции
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Архитектура Backend
|
||||
|
||||
### API Endpoints
|
||||
|
||||
```
|
||||
# Схема
|
||||
GET /api/schema/:deviceId — получить детальную схему устройства
|
||||
PUT /api/schema/:deviceId/layout — сохранить layout (позиции, порты, карты)
|
||||
|
||||
# Сайты
|
||||
GET /api/sites — список сайтов (с фильтрацией)
|
||||
GET /api/sites/:id — карточка сайта
|
||||
POST /api/sites — создать сайт
|
||||
PUT /api/sites/:id — обновить сайт
|
||||
DELETE /api/sites/:id — удалить сайт
|
||||
|
||||
# Устройства
|
||||
GET /api/devices — список устройств (фильтры: тип, сайт)
|
||||
GET /api/devices/:id — карточка устройства
|
||||
POST /api/devices — создать устройство
|
||||
PUT /api/devices/:id — обновить устройство
|
||||
DELETE /api/devices/:id — удалить устройство
|
||||
POST /api/devices/:id/copy — копирование (body: { withConnections: boolean })
|
||||
PUT /api/devices/:id/move-site — перенос на другой сайт
|
||||
|
||||
# Карты
|
||||
GET /api/devices/:deviceId/cards — карты устройства
|
||||
PUT /api/cards/:id/visibility — показать/скрыть карту
|
||||
|
||||
# Порты
|
||||
GET /api/devices/:deviceId/ports — порты устройства
|
||||
PUT /api/ports/order — сохранить порядок портов
|
||||
PUT /api/ports/:id/settings — настройки порта (цвет, сторона)
|
||||
|
||||
# Линии
|
||||
GET /api/lines — линии (фильтры: device, site)
|
||||
GET /api/lines/:id — карточка линии
|
||||
POST /api/lines — создать линию
|
||||
PUT /api/lines/:id — обновить линию
|
||||
DELETE /api/lines/:id — удалить линию
|
||||
POST /api/lines/:id/break — разрыв линии
|
||||
POST /api/lines/:id/copy — копирование линии
|
||||
|
||||
# Блокировки
|
||||
POST /api/locks — заблокировать элементы
|
||||
DELETE /api/locks/:id — разблокировать
|
||||
GET /api/locks/schema/:deviceId — текущие блокировки на схеме
|
||||
|
||||
# Экспорт
|
||||
GET /api/export/excel/:deviceId — экспорт в Excel
|
||||
```
|
||||
|
||||
### Модель данных (TypeORM Entities)
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Site │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ name: string │
|
||||
│ address: string │
|
||||
│ erpCode: string │
|
||||
│ code1C: string │
|
||||
│ status: SiteStatus │
|
||||
│ parentSiteId: UUID? │
|
||||
│ ──────────────────── │
|
||||
│ devices: Device[] │
|
||||
│ childSites: Site[] │
|
||||
└──────────────────────┘
|
||||
│ 1:N
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Device │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ name: string │
|
||||
│ networkName: string │
|
||||
│ ipAddress: string │
|
||||
│ marking: string │
|
||||
│ id1: string │
|
||||
│ id2: string │
|
||||
│ type: DeviceType │
|
||||
│ group: DeviceGroup │ // 'active' | 'passive' | 'cross' | 'splice'
|
||||
│ category: string │ // для маппинга в слой
|
||||
│ status: DeviceStatus │
|
||||
│ siteId: UUID │
|
||||
│ ──────────────────── │
|
||||
│ cards: Card[] │
|
||||
│ ports: Port[] │
|
||||
│ site: Site │
|
||||
└──────────────────────┘
|
||||
│ 1:N
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Card │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ slotName: string │
|
||||
│ networkName: string │
|
||||
│ status: CardStatus │
|
||||
│ visible: boolean │
|
||||
│ deviceId: UUID │
|
||||
│ ──────────────────── │
|
||||
│ ports: Port[] │
|
||||
│ device: Device │
|
||||
└──────────────────────┘
|
||||
│ 1:N
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Port │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ name: string │
|
||||
│ slotName: string │
|
||||
│ side: 'left' | 'right'│
|
||||
│ sortOrder: number │
|
||||
│ labelColor: string │
|
||||
│ deviceId: UUID │
|
||||
│ cardId: UUID? │
|
||||
│ ──────────────────── │
|
||||
│ device: Device │
|
||||
│ card: Card? │
|
||||
│ linesA: Line[] │
|
||||
│ linesZ: Line[] │
|
||||
└──────────────────────┘
|
||||
|
||||
┌──────────────────────┐
|
||||
│ Line │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ name: string │
|
||||
│ templateName: string │
|
||||
│ status: LineStatus │
|
||||
│ type: LineType │ // simple | complex
|
||||
│ medium: Medium │ // optical | copper | wireless | unknown
|
||||
│ lineStyle: LineStyle │ // solid | dashed | dotted
|
||||
│ portAId: UUID │
|
||||
│ portZId: UUID │
|
||||
│ ──────────────────── │
|
||||
│ portA: Port │
|
||||
│ portZ: Port │
|
||||
│ fibers: Fiber[] │ // для сложных линий
|
||||
└──────────────────────┘
|
||||
|
||||
┌──────────────────────┐
|
||||
│ Fiber │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ name: string │
|
||||
│ status: FiberStatus │
|
||||
│ lineId: UUID │
|
||||
│ portAId: UUID │
|
||||
│ portZId: UUID │
|
||||
└──────────────────────┘
|
||||
|
||||
┌──────────────────────┐
|
||||
│ SchemaLayout │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ userId: UUID │
|
||||
│ deviceId: UUID │ // центральное устройство схемы
|
||||
│ positions: jsonb │ // { nodeId: {x, y, w, h} }
|
||||
│ portOrders: jsonb │ // { deviceId: { portId: sortOrder } }
|
||||
│ cardVisibility: jsonb │ // { cardId: boolean }
|
||||
│ settings: jsonb │ // настройки отображения
|
||||
│ updatedAt: Date │
|
||||
└──────────────────────┘
|
||||
|
||||
┌──────────────────────┐
|
||||
│ EditLock │
|
||||
├──────────────────────┤
|
||||
│ id: UUID │
|
||||
│ schemaDeviceId: UUID │ // схема которую заблокировали
|
||||
│ userId: UUID │
|
||||
│ userName: string │
|
||||
│ lockedAt: Date │
|
||||
│ expiresAt: Date │ // автоистечение через N минут
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Enums и цветовая карта
|
||||
|
||||
```typescript
|
||||
// Статусы (для цветовой карты — Appendix A)
|
||||
enum EntityStatus {
|
||||
Active = 'active',
|
||||
Planned = 'planned',
|
||||
UnderConstruction = 'under_construction',
|
||||
Reserved = 'reserved',
|
||||
Faulty = 'faulty',
|
||||
Decommissioned = 'decommissioned',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
// Типы устройств (для маппинга в слои)
|
||||
enum DeviceCategory {
|
||||
CrossOptical = 'cross_optical',
|
||||
CrossCopper = 'cross_copper',
|
||||
RRL = 'rrl',
|
||||
Wireless = 'wireless',
|
||||
Satellite = 'satellite',
|
||||
TSPU = 'tspu',
|
||||
DWDM = 'dwdm',
|
||||
MEN = 'men',
|
||||
SDH = 'sdh',
|
||||
MultiservicePlatform = 'multiservice_platform',
|
||||
IP = 'ip',
|
||||
OpticalModem = 'optical_modem',
|
||||
OpticalMux = 'optical_mux',
|
||||
LanWlan = 'lan_wlan',
|
||||
RanController = 'ran_controller',
|
||||
MGN = 'mgn',
|
||||
MGX = 'mgx',
|
||||
Server = 'server',
|
||||
SORM = 'sorm',
|
||||
MOB = 'mob',
|
||||
FIX = 'fix',
|
||||
VOIP = 'voip',
|
||||
xDSL = 'xdsl',
|
||||
PDH = 'pdh',
|
||||
RanBaseStation = 'ran_base_station',
|
||||
Unknown = 'unknown',
|
||||
VideoSurveillance = 'video_surveillance',
|
||||
}
|
||||
|
||||
// Группы устройств
|
||||
enum DeviceGroup {
|
||||
Active = 'active',
|
||||
Passive = 'passive',
|
||||
}
|
||||
|
||||
// Среда передачи
|
||||
enum Medium {
|
||||
Optical = 'optical',
|
||||
Copper = 'copper',
|
||||
Wireless = 'wireless',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ключевые архитектурные решения
|
||||
|
||||
### 1. Рендеринг нод: SVG vs HTML
|
||||
|
||||
- **Сайт (SiteNode):** SVG `Shape.Rect` с кастомным markup. Шапка — `<foreignObject>` для текстового переноса.
|
||||
- **Кросс:** SVG `<path>` для асимметричного скругления.
|
||||
- **Карты и порты:** SVG элементы, вложенные через X6 embedding (parent-child).
|
||||
|
||||
### 2. Вложенность (Embedding)
|
||||
|
||||
X6 нативно поддерживает `embedding`:
|
||||
```typescript
|
||||
graph.options.embedding = {
|
||||
enabled: true,
|
||||
findParent: 'bbox', // или кастомная функция
|
||||
};
|
||||
```
|
||||
- Сайт → устройства: `device.setParent(site)`
|
||||
- Устройство → карты: `card.setParent(device)`
|
||||
- Карта → порты: `port.setParent(card)` (через X6 ports API)
|
||||
|
||||
### 3. Группировка линий
|
||||
|
||||
Реализуется на уровне рендеринга:
|
||||
1. При загрузке данных — группировка линий по парам `(deviceA, deviceZ)`
|
||||
2. Если count > 1 → рендеринг одной «групповой» линии с label-счётчиком
|
||||
3. По клику «Разгруппировать» → замена на N отдельных линий с разбивкой по статусам
|
||||
|
||||
### 4. Автолейаут
|
||||
|
||||
Кастомный алгоритм (не dagre/elk):
|
||||
1. Парсинг устройств сайта → распределение по 8 слоям
|
||||
2. Расчёт геометрии слоёв (Y offset, высота по максимальному элементу)
|
||||
3. Размещение устройств внутри слоя (X offset, горизонтальный ряд)
|
||||
4. Схлопывание пустых слоёв
|
||||
5. Позиционирование дочерних сайтов по нижней границе родителя
|
||||
|
||||
### 5. Блокировка / Concurrency
|
||||
|
||||
- При переходе в режим редактирования → `POST /api/locks` (pessimistic lock)
|
||||
- Блокировка с TTL (auto-expire через 30 минут неактивности)
|
||||
- Heartbeat каждые 60 секунд для продления блокировки
|
||||
- Другие пользователи видят значок блокировки (overlay на нодах)
|
||||
- При сохранении → `DELETE /api/locks/:id`
|
||||
|
||||
### 6. Сохранение состояния
|
||||
|
||||
- Позиции, порядок портов, видимость карт — JSON в таблице `schema_layout`
|
||||
- Привязка к паре `(userId, deviceId)` — у каждого пользователя свой layout
|
||||
- При отсутствии сохранённого layout — применяется автолейаут
|
||||
|
||||
---
|
||||
|
||||
## Зависимости Frontend
|
||||
|
||||
| Пакет | Назначение |
|
||||
|-------|-----------|
|
||||
| `@antv/x6` | Основная библиотека графов |
|
||||
| `@antv/x6-plugin-selection` | Выделение (rectangle, rubberband) |
|
||||
| `@antv/x6-plugin-snapline` | Привязка к сетке |
|
||||
| `@antv/x6-plugin-minimap` | Мини-карта |
|
||||
| `@antv/x6-plugin-keyboard` | Горячие клавиши |
|
||||
| `@antv/x6-plugin-clipboard` | Буфер обмена |
|
||||
| `@antv/x6-plugin-history` | Undo/Redo |
|
||||
| `@antv/x6-plugin-export` | Экспорт PNG |
|
||||
| `antd` | UI-компоненты (меню, модалки, таблицы, формы) |
|
||||
| `zustand` | State management |
|
||||
| `axios` | HTTP-клиент |
|
||||
| `xlsx` | Экспорт Excel |
|
||||
|
||||
---
|
||||
|
||||
## Фазы реализации
|
||||
|
||||
### Фаза 1: Каркас и базовые ноды
|
||||
- Инициализация проекта (frontend + backend)
|
||||
- Настройка X6 Graph с базовыми плагинами
|
||||
- Custom Nodes: SiteNode, OtherDeviceNode (простые)
|
||||
- Базовые Entity и CRUD (sites, devices)
|
||||
- Загрузка и отображение статической схемы
|
||||
|
||||
### Фаза 2: Порты и линии
|
||||
- Система портов (двухконтейнерная, сортировка)
|
||||
- CardNode с вложенными портами
|
||||
- Линии с привязкой к портам
|
||||
- Стили линий по статусу и среде передачи
|
||||
- CrossDeviceNode, SpliceClosureNode
|
||||
|
||||
### Фаза 3: Интерактивность
|
||||
- Контекстные меню (6 типов)
|
||||
- Drag-and-drop устройств
|
||||
- Создание линий (4 способа)
|
||||
- Выделение (rectangle, lasso)
|
||||
- Автолейаут (8 слоёв)
|
||||
|
||||
### Фаза 4: Панели и UI
|
||||
- Toolbar с настройками
|
||||
- Левая панель (дерево навигации)
|
||||
- Правая панель (карточки объектов)
|
||||
- Нижняя панель соединений
|
||||
|
||||
### Фаза 5: Режимы работы и persistence
|
||||
- Режим просмотра / редактирования
|
||||
- Блокировки (concurrency)
|
||||
- Сохранение layout (позиции, порты, карты)
|
||||
- Копирование/вставка со связями
|
||||
- Разрыв линии
|
||||
|
||||
### Фаза 6: Группировка, экспорт, полировка
|
||||
- Группировка линий (счётчик, тултип, разгруппировка)
|
||||
- Экспорт PNG и Excel
|
||||
- Перемычки (self-loops)
|
||||
- Snap-to-grid, MiniMap
|
||||
- Тестирование, оптимизация производительности
|
||||
Reference in New Issue
Block a user