Панель подробностей
Клик по узлу открывает перетаскиваемое окно. Пакет наполняет его по минимуму — название, тип, табы и плоский список полей meta, — потому что не знает ни имён ваших полей, ни того, какие из них важны. Настоящее содержимое идёт в слот #details.
Показать исходник
<script setup lang="ts">
import { ref } from 'vue'
import { AssetMap, DetailsSection } from 'vue-asset-map'
import { useSiteTheme } from '../theme/useSiteTheme'
import { useSiteLocale } from '../theme/useSiteLocale'
import type { AssetMeta } from './fixture'
import { entities, links, views, statusColors, kindIcons } from './fixture'
// Opened by default so the custom panel is visible without a click — a real
// app would not do this, but a demo should not make "click a node first" a
// precondition for seeing what it demonstrates.
const openDetails = ref(['ws-artjom'])
const selection = ref(['ws-artjom'])
const theme = useSiteTheme()
const locale = useSiteLocale()
// Field labels are the consumer's business: the package never guesses what
// lives in `meta`.
const LABELS: Record<keyof AssetMeta & string, string> = {
owner: 'Owner',
os: 'OS',
externalIp: 'External IP',
overlayIp: 'Overlay IP',
fqdn: 'FQDN',
cpu: 'vCPU',
ramGb: 'RAM, GB',
location: 'Location',
subnet: 'Subnet',
assetType: 'Asset type',
severity: 'Severity',
tags: 'Tags',
}
function rows(meta: AssetMeta | undefined) {
if (!meta) return []
return Object.entries(meta)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => ({
key,
label: LABELS[key as keyof typeof LABELS] ?? key,
value: Array.isArray(value) ? value.join(', ') : String(value),
}))
}
</script>
<template>
<AssetMap
v-model:open-details="openDetails"
v-model:selection="selection"
class="demo-map"
view="physical"
:entities="entities"
:links="links"
:views="views"
:theme="theme"
:locale="locale"
:theme-config="{ statusColors, kindIcons }"
:layout="{ engine: 'radial', options: { rootId: 'internet' } }"
auto-fit-view
>
<template #details="{ entity }">
<DetailsSection title="Inventory">
<div v-for="row in rows(entity.meta)" :key="row.key" class="details-row">
<span class="details-row__key">{{ row.label }}</span>
<span class="details-row__value">{{ row.value }}</span>
</div>
</DetailsSection>
</template>
</AssetMap>
</template>
<style scoped>
.details-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 3px 0;
font-size: 13px;
}
.details-row__key {
color: var(--vp-c-text-2);
}
</style>
Наполнение окна
<script setup lang="ts">
import { AssetMap, DetailsSection } from 'vue-asset-map'
const LABELS = { owner: 'Владелец', os: 'ОС', cpu: 'vCPU' }
</script>
<template>
<AssetMap :entities="entities" :links="links">
<template #details="{ entity, close, tab }">
<DetailsSection title="Инвентаризация">
<div v-for="(label, key) in LABELS" :key="key">
<span>{{ label }}</span>
<span>{{ entity.meta?.[key] ?? '—' }}</span>
</div>
</DetailsSection>
<button type="button" @click="close">Закрыть</button>
</template>
</AssetMap>
</template>DetailsSection экспортируется, чтобы свою панель можно было собрать из тех же блоков, что и дефолтную, и не подбирать отступы и типографику на глаз.
Полностью своё оформление
Окно подробностей — это четыре части одна под другой: шапка (.vam-window__header), заголовочный блок (.vam-window__heading), полоса вкладок (.vam-window__tabs) и тело (.vam-window__content). Всё, что ниже шапки, можно подменить или убрать совсем, и останется только одна шапка:
| Часть | Замена | Убрать |
|---|---|---|
| Шапка | detailsTitle | — |
| Подпись в шапке | #details-title | — |
| Заголовочный блок | #details-header | showDetailsHeading: false |
| Полоса вкладок | #details-tabs | detailsTabs: false |
| Состав вкладок | detailsTabs | — |
| Тело | #details | — |
По умолчанию окно называется entity.label. detailsTitle меняет название — строкой или функцией от сущности:
<AssetMap
:entities="entities"
:links="links"
:details-title="(entity) => `${entity.type.toUpperCase()} · ${entity.label}`"
/>#details-title забирает подпись себе целиком — не строкой, а разметкой, — и получает вместе с сущностью состояние collapsed:
<template #details-title="{ entity, collapsed }">
<b>{{ entity.label }}</b>
<small v-if="!collapsed">{{ entity.type }}</small>
</template>#details-header подменяет заголовочный блок под шапкой — крупное название со строкой типа и состояния. showDetailsHeading: false его убирает.
Всё вместе оставляет от дефолтного оформления одну шапку:
<AssetMap
:entities="entities"
:links="links"
details-title="Карточка актива"
:show-details-heading="false"
:details-tabs="[{ id: 'risk', label: 'Риски' }]"
>
<template #details-tabs="{ tabs, active, select }">
<nav class="my-tabs">
<button v-for="tab in tabs" :key="tab.id" :class="{ on: tab.id === active }" @click="select(tab.id)">
{{ tab.label }}
</button>
</nav>
</template>
<template #details="{ entity, tab }">
<MyPanel :entity="entity" :tab="tab" />
</template>
</AssetMap>Сама шапка остаётся: на ней держатся перетаскивание, сжатие и закрытие — то, что делает это окном. Если своей должна быть и шапка, соберите окно из MapWindow в слоте #windows — об этом ниже.
У соединений тоже есть окно
Клик по связи открывает его; управляется оно через v-model:openLinks, а содержимое даёт слот #link-details. Каркас, лимит и угол у него те же. А с editable у обоих окон появляется кнопка «Редактировать», превращающая тело в форму. И то и другое — в Соединениях и правке.
Несколько окон сразу
maxDetailsWindows ограничивает, сколько окон остаётся открытыми; по умолчанию 2, а 0 выключает окна полностью, не убирая остальной интерфейс. При достижении лимита открытие следующего закрывает самое старое — молча проигнорировать клик хуже: пользователь решит, что карта не отвечает.
Окно можно не закрывать, а сжать до шапки. Escape закрывает верхнее, по одному за нажатие.
Набор открытых окон — такое же состояние, как остальные:
<AssetMap v-model:open-details="openDetails" :entities="entities" :links="links" />Свои окна
MapWindow экспортируется: шапка, перетаскивание, сжатие и закрытие, а позиционирование остаётся за вызывающей стороной.
<script setup lang="ts">
import { AssetMap, MapWindow } from 'vue-asset-map'
</script>
<template>
<AssetMap :entities="entities" :links="links">
<template #windows>
<MapWindow
title="История"
:closable="false"
:collapsible="false"
drag-handle
header-only
:style="{ position: 'absolute', top: '16px', right: '16px' }"
>
<template #header>
<button type="button">⏮</button>
<button type="button">⏭</button>
</template>
</MapWindow>
</template>
</AssetMap>
</template>| Prop | По умолчанию | Зачем |
|---|---|---|
title | — | Подпись по центру шапки. |
closable | true | false для окон, которые закрывать нельзя. |
collapsible | true | Кнопка сжатия до шапки. |
headerOnly | false | Только шапка, без тела. |
dragHandle | false | Ручка перетаскивания — нужна окнам без заголовка. |
offset | — | Начальный сдвиг в пикселях, чтобы окна не накладывались друг на друга. |
Окна из слота #windows рисуются поверх окон подробностей, а окна подробностей замеряют их и начинаются ниже — новое окно не закроет вашу «историю». И те и другие опускаются под тулбар, если в #toolbar-right что-то есть, чтобы кнопки приложения оставались доступными.
Интерактивные элементы внутри шапки не являются ручкой перетаскивания: кнопки, ссылки и поля получают свои клики.
Табы
В дефолтной панели два таба: обзор и связи. Id активного таба доходит до слота #details как tab, поэтому своя панель может рисовать разное содержимое. Он никогда не сбрасывается при смене сущности: карта рендерит окна с key по id сущности, поэтому другая сущность — это другой экземпляр компонента с состоянием по умолчанию.
detailsTabs заменяет эту пару своими табами. label — готовая строка, а не ключ локали: ваши табы — ваши, и словарь пакета о них ничего не знает. Первый в списке открыт сразу.
<AssetMap
:entities="entities"
:links="links"
:details-tabs="[
{ id: 'risk', label: 'Риски' },
{ id: 'audit', label: 'Аудит' },
]"
>
<template #details="{ entity, tab }">
<RiskPanel v-if="tab === 'risk'" :entity="entity" />
<AuditPanel v-else :entity="entity" />
</template>
</AssetMap>Со своими табами пакет своего тела не рисует: под незнакомым табом ему нечего показать, а подставить под «Риски» встроенный список связей было бы прямой ложью. Заполняйте #details вместе с ними.
detailsTabs: false убирает полосу. Встроенное тело при этом остаётся — табы убраны, а не подменены, — поэтому для окна без вкладок больше ничего не нужно.
#details-tabs подменяет отрисовку полосы, сохраняя поведение: слот получает табы, id активного и колбэк select.