в меню иконки items соответствуют их типу

This commit is contained in:
Илья Братских 2026-06-28 16:54:25 +03:00 committed by KBAHTNET
parent f3508bf7f9
commit c1547cc24e
9 changed files with 88 additions and 17 deletions

View File

@ -250,8 +250,9 @@ ecosystem/
| | `tags` | Массив строк для поиска |
| | `id` | Стабильный ключ (DOM, фокус из дерева) |
| | `number` | Отображаемый номер |
| | `icon` | Опционально: свой путь к SVG (иначе — иконка из `type`) |
- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON
- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON; определяет иконку и подпись на карточке
- `id` элемента нужен, если нет `number` — иначе ключ генерируется автоматически
### Примеры данных
@ -283,6 +284,8 @@ npx serve .
`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp`
Каждый тип имеет собственную SVG в `assets/` (например, `postgresql.svg`, `cloudbeaver.svg`). Иконка показывается на карточке каталога и в дереве навигации.
Переопределение или добавление типов в JSON:
```json

1
assets/cloudbeaver.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M34 88c0-18 14-32 32-34 4-18 20-30 38-30 22 0 40 16 40 38 0 2 0 4-.2 6 14 4 24 16 24 32 0 18-14 32-32 32H52c-10 0-18-8-18-18v-26z" fill="white" fill-opacity=".95"/><ellipse cx="80" cy="92" rx="34" ry="14" fill="white"/><path d="M46 92v24c0 8 15 14 34 14s34-6 34-14V92" fill="white" fill-opacity=".88"/><path d="M46 104c0 8 15 14 34 14s34-6 34-14" stroke="#1565C0" stroke-width="6"/><circle cx="66" cy="72" r="8" fill="#1565C0"/><circle cx="94" cy="72" r="8" fill="#1565C0"/><path d="M72 86h16" stroke="#1565C0" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#64B5F6"/><stop offset="1" stop-color="#1565C0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 876 B

1
assets/postgresql.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 98c0-30 15-52 34-52s34 22 34 52c0 14-9 26-21 30v16H67v-16c-12-4-21-16-21-30z" fill="white"/><circle cx="69" cy="78" r="6" fill="#336791"/><circle cx="91" cy="78" r="6" fill="#336791"/><path d="M46 98c-10 10-16 24-16 38h18c2-14 9-26 18-32" fill="white" fill-opacity=".92"/><path d="M72 92c4 3 8 3 12 0" stroke="#336791" stroke-width="5" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5B9BD5"/><stop offset="1" stop-color="#336791"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 688 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M34 88c0-18 14-32 32-34 4-18 20-30 38-30 22 0 40 16 40 38 0 2 0 4-.2 6 14 4 24 16 24 32 0 18-14 32-32 32H52c-10 0-18-8-18-18v-26z" fill="white" fill-opacity=".95"/><ellipse cx="80" cy="92" rx="34" ry="14" fill="white"/><path d="M46 92v24c0 8 15 14 34 14s34-6 34-14V92" fill="white" fill-opacity=".88"/><path d="M46 104c0 8 15 14 34 14s34-6 34-14" stroke="#1565C0" stroke-width="6"/><circle cx="66" cy="72" r="8" fill="#1565C0"/><circle cx="94" cy="72" r="8" fill="#1565C0"/><path d="M72 86h16" stroke="#1565C0" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#64B5F6"/><stop offset="1" stop-color="#1565C0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 98c0-30 15-52 34-52s34 22 34 52c0 14-9 26-21 30v16H67v-16c-12-4-21-16-21-30z" fill="white"/><circle cx="69" cy="78" r="6" fill="#336791"/><circle cx="91" cy="78" r="6" fill="#336791"/><path d="M46 98c-10 10-16 24-16 38h18c2-14 9-26 18-32" fill="white" fill-opacity=".92"/><path d="M72 92c4 3 8 3 12 0" stroke="#336791" stroke-width="5" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5B9BD5"/><stop offset="1" stop-color="#336791"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 688 B

View File

@ -33,7 +33,7 @@ let panelTitle = "Панель экосистемы";
const defaultServiceMeta = {
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
config: { label: "Config", icon: "assets/config.svg" },
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
@ -42,7 +42,7 @@ const defaultServiceMeta = {
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
messages: { label: "Messages", icon: "assets/messages.svg" },
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
postgresql: { label: "PostgreSQL", icon: "assets/postgresql.svg" },
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
@ -614,9 +614,23 @@ function normalizeItemFromConfig(item, index = 0) {
type: String(item.type),
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
icon: typeof item.icon === "string" && item.icon.trim() ? item.icon.trim() : "",
};
}
function getItemMeta(item) {
const typeMeta = serviceMeta[item.type] || serviceMeta.config;
if (item.icon) {
return {
label: typeMeta.label,
icon: item.icon,
};
}
return typeMeta;
}
function slugifyItemKey(value) {
return normalize(String(value))
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
@ -782,7 +796,7 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
// Создает конечную карточку навигации.
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
function createCardElement(item, query = "") {
const meta = serviceMeta[item.type] || serviceMeta.config;
const meta = getItemMeta(item);
const link = document.createElement("a");
link.className = "nav-card";
link.href = item.url;
@ -950,11 +964,16 @@ function createTreeItemList(items, query = "", className = "tree-items") {
itemList.className = className;
items.forEach((item) => {
const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
itemButton.innerHTML = `<span>${highlightText(item.title, query)}</span><small>${highlightText(item.tags.join(", "), query)}</small>`;
itemButton.innerHTML = `
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
<span>${highlightText(item.title, query)}</span>
<small>${highlightText(item.tags.join(", "), query)}</small>
`;
itemList.append(itemButton);
});

View File

@ -325,11 +325,17 @@ html[data-tree-two-column="true"] .tree-items {
}
html[data-tree-two-column="true"] .tree-node.tree-file {
grid-template-columns: 16px minmax(0, 1fr);
grid-template-columns: 20px minmax(0, 1fr);
min-height: 24px;
font-size: 0.8rem;
}
html[data-tree-two-column="true"] .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
html[data-tree-two-column="true"] .tree-node.tree-file small {
display: none;
}
@ -930,12 +936,19 @@ html[data-theme="light"] .accent-swatch-custom {
}
.tree-file {
grid-template-columns: 18px minmax(0, 1fr) auto;
grid-template-columns: 20px minmax(0, 1fr) auto;
}
.tree-file::before {
content: "📄";
font-size: 0.86rem;
content: none;
}
.tree-node-icon {
width: 18px;
height: 18px;
border-radius: 5px;
object-fit: cover;
display: block;
}
/* Контейнер элементов внутри группы дерева. */

View File

@ -33,7 +33,7 @@ let panelTitle = "Панель экосистемы";
const defaultServiceMeta = {
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
config: { label: "Config", icon: "assets/config.svg" },
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
@ -42,7 +42,7 @@ const defaultServiceMeta = {
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
messages: { label: "Messages", icon: "assets/messages.svg" },
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
postgresql: { label: "PostgreSQL", icon: "assets/postgresql.svg" },
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
@ -614,9 +614,23 @@ function normalizeItemFromConfig(item, index = 0) {
type: String(item.type),
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
icon: typeof item.icon === "string" && item.icon.trim() ? item.icon.trim() : "",
};
}
function getItemMeta(item) {
const typeMeta = serviceMeta[item.type] || serviceMeta.config;
if (item.icon) {
return {
label: typeMeta.label,
icon: item.icon,
};
}
return typeMeta;
}
function slugifyItemKey(value) {
return normalize(String(value))
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
@ -782,7 +796,7 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
// Создает конечную карточку навигации.
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
function createCardElement(item, query = "") {
const meta = serviceMeta[item.type] || serviceMeta.config;
const meta = getItemMeta(item);
const link = document.createElement("a");
link.className = "nav-card";
link.href = item.url;
@ -950,11 +964,16 @@ function createTreeItemList(items, query = "", className = "tree-items") {
itemList.className = className;
items.forEach((item) => {
const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
itemButton.innerHTML = `<span>${highlightText(item.title, query)}</span><small>${highlightText(item.tags.join(", "), query)}</small>`;
itemButton.innerHTML = `
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
<span>${highlightText(item.title, query)}</span>
<small>${highlightText(item.tags.join(", "), query)}</small>
`;
itemList.append(itemButton);
});

View File

@ -325,11 +325,17 @@ html[data-tree-two-column="true"] .tree-items {
}
html[data-tree-two-column="true"] .tree-node.tree-file {
grid-template-columns: 16px minmax(0, 1fr);
grid-template-columns: 20px minmax(0, 1fr);
min-height: 24px;
font-size: 0.8rem;
}
html[data-tree-two-column="true"] .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
html[data-tree-two-column="true"] .tree-node.tree-file small {
display: none;
}
@ -930,12 +936,19 @@ html[data-theme="light"] .accent-swatch-custom {
}
.tree-file {
grid-template-columns: 18px minmax(0, 1fr) auto;
grid-template-columns: 20px minmax(0, 1fr) auto;
}
.tree-file::before {
content: "📄";
font-size: 0.86rem;
content: none;
}
.tree-node-icon {
width: 18px;
height: 18px;
border-radius: 5px;
object-fit: cover;
display: block;
}
/* Контейнер элементов внутри группы дерева. */