mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 00:00:50 +00:00
feat: optimize node switch logic for large node sets (#12928)
This commit is contained in:
@@ -223,7 +223,7 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
|
||||
}
|
||||
}
|
||||
|
||||
dropWithExclude(path.Join(global.Dir.BaseDir, uploadPath), []string{"theme"}, taskItem, &size, &fileCount)
|
||||
dropWithExclude(path.Join(global.Dir.BaseDir, uploadPath), []string{"theme", "skills-hub"}, taskItem, &size, &fileCount)
|
||||
dropWithTask(path.Join(global.Dir.BaseDir, downloadPath), taskItem, &size, &fileCount)
|
||||
|
||||
logFiles, _ := os.ReadDir(global.Dir.LogDir)
|
||||
@@ -863,7 +863,7 @@ func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string,
|
||||
return lists
|
||||
}
|
||||
for _, file := range files {
|
||||
if treeType == "upload" && (file.Name() == "theme" && file.IsDir()) {
|
||||
if treeType == "upload" && ((file.Name() == "theme" || file.Name() == "skills-hub") && file.IsDir()) {
|
||||
continue
|
||||
}
|
||||
if treeType == "system_log" && (file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir()) {
|
||||
|
||||
@@ -307,11 +307,14 @@ export namespace Setting {
|
||||
}
|
||||
export interface NodeItem {
|
||||
id: number;
|
||||
groupID?: number;
|
||||
groupBelong?: string;
|
||||
addr: string;
|
||||
status: string;
|
||||
version: string;
|
||||
isXpack: boolean;
|
||||
isBound: boolean;
|
||||
isFavorite?: boolean;
|
||||
name: string;
|
||||
}
|
||||
export interface SimpleNodeItem {
|
||||
|
||||
@@ -43,6 +43,9 @@ export const loadLicenseOptions = () => {
|
||||
export const listNodeOptions = (type: string) => {
|
||||
return http.post<Array<Setting.NodeItem>>(`/core/nodes/list`, { type: type });
|
||||
};
|
||||
export const updateNodeFavorite = (id: number, isFavorite: boolean) => {
|
||||
return http.post(`/core/xpack/nodes/favorite`, { id, isFavorite });
|
||||
};
|
||||
export const listAllSimpleNodes = () => {
|
||||
return http.get<Array<Setting.SimpleNodeItem>>(`/core/nodes/simple/all`);
|
||||
};
|
||||
|
||||
@@ -5126,6 +5126,8 @@ const message = {
|
||||
memTotal: 'Total Memory',
|
||||
nodeManagement: 'Multi-Machine Management',
|
||||
multiOverview: 'Multi-Machine',
|
||||
commonNodes: 'Frequently Used Nodes',
|
||||
searchNodePlaceholder: 'Search node name, group, or address',
|
||||
nodeItem: 'Node Management',
|
||||
panelItem: 'Panel Management',
|
||||
addPanel: 'Add Panel',
|
||||
|
||||
@@ -5177,6 +5177,8 @@ const message = {
|
||||
memTotal: 'Memoria Total',
|
||||
nodeManagement: 'Gestión Multi-Máquina',
|
||||
multiOverview: 'Multimáquina',
|
||||
commonNodes: 'Nodos frecuentes',
|
||||
searchNodePlaceholder: 'Buscar nombre, grupo o dirección del nodo',
|
||||
nodeItem: 'Gestión de Nodos',
|
||||
panelItem: 'Gestión de Paneles',
|
||||
addNode: 'Añadir Nodo',
|
||||
|
||||
@@ -5146,6 +5146,8 @@ const message = {
|
||||
memTotal: '総メモリ',
|
||||
nodeManagement: 'マルチマシン管理',
|
||||
multiOverview: 'マルチマシン',
|
||||
commonNodes: 'よく使うノード',
|
||||
searchNodePlaceholder: 'ノード名、グループ、アドレスを検索',
|
||||
nodeItem: 'ノード管理',
|
||||
panelItem: 'パネル管理',
|
||||
addPanel: 'パネル追加',
|
||||
|
||||
@@ -5036,6 +5036,8 @@ const message = {
|
||||
memTotal: '총 메모리',
|
||||
nodeManagement: '다중 머신 관리',
|
||||
multiOverview: '다중 머신',
|
||||
commonNodes: '자주 사용하는 노드',
|
||||
searchNodePlaceholder: '노드 이름, 그룹 또는 주소 검색',
|
||||
nodeItem: '노드 관리',
|
||||
panelItem: '패널 관리',
|
||||
addPanel: '패널 추가',
|
||||
|
||||
@@ -5204,6 +5204,8 @@ const message = {
|
||||
memTotal: 'Jumlah Memori',
|
||||
nodeManagement: 'Pengurusan Multi-Mesin',
|
||||
multiOverview: 'Multi-Mesin',
|
||||
commonNodes: 'Nod Kerap Digunakan',
|
||||
searchNodePlaceholder: 'Cari nama nod, kumpulan atau alamat',
|
||||
nodeItem: 'Pengurusan Nod',
|
||||
panelItem: 'Pengurusan Panel',
|
||||
addPanel: 'Tambah Panel',
|
||||
|
||||
@@ -5352,6 +5352,8 @@ const message = {
|
||||
memTotal: 'Memória Total',
|
||||
nodeManagement: 'Gerenciamento Multi-Máquina',
|
||||
multiOverview: 'Multi-Máquina',
|
||||
commonNodes: 'Nós frequentes',
|
||||
searchNodePlaceholder: 'Pesquise nome, grupo ou endereço do nó',
|
||||
nodeItem: 'Gerenciamento de Nós',
|
||||
panelItem: 'Gerenciamento de Painéis',
|
||||
addPanel: 'Adicionar Painel',
|
||||
|
||||
@@ -5207,6 +5207,8 @@ const message = {
|
||||
memTotal: 'Общая память',
|
||||
nodeManagement: 'Управление Несколькими Машинами',
|
||||
multiOverview: 'Несколько Машин',
|
||||
commonNodes: 'Часто используемые узлы',
|
||||
searchNodePlaceholder: 'Поиск по имени узла, группе или адресу',
|
||||
nodeItem: 'Управление Узлами',
|
||||
panelItem: 'Управление Панелями',
|
||||
addPanel: 'Добавить Панель',
|
||||
|
||||
@@ -5194,6 +5194,8 @@ const message = {
|
||||
memTotal: 'Toplam Bellek',
|
||||
nodeManagement: 'Çoklu Makine Yönetimi',
|
||||
multiOverview: 'Çoklu Makine',
|
||||
commonNodes: 'Sık Kullanılan Düğümler',
|
||||
searchNodePlaceholder: 'Düğüm adı, grup veya adres ara',
|
||||
nodeItem: 'Düğüm Yönetimi',
|
||||
panelItem: 'Panel Yönetimi',
|
||||
addPanel: 'Panel Ekle',
|
||||
|
||||
@@ -4768,6 +4768,8 @@ const message = {
|
||||
memTotal: '記憶體總計',
|
||||
nodeManagement: '多機管理',
|
||||
multiOverview: '多機概覽',
|
||||
commonNodes: '常用節點',
|
||||
searchNodePlaceholder: '搜尋節點名稱、群組或地址',
|
||||
nodeItem: '節點管理',
|
||||
panelItem: '面板管理',
|
||||
addPanel: '添加面板',
|
||||
|
||||
@@ -4219,6 +4219,8 @@ const message = {
|
||||
memTotal: '内存总计',
|
||||
nodeManagement: '多机管理',
|
||||
multiOverview: '多机概览',
|
||||
commonNodes: '常用节点',
|
||||
searchNodePlaceholder: '搜索节点名称、分组或地址',
|
||||
nodeItem: '节点管理',
|
||||
panelItem: '面板管理',
|
||||
addPanel: '添加面板',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover
|
||||
v-model:visible="popoverVisible"
|
||||
placement="right-end"
|
||||
:show-arrow="false"
|
||||
:offset="0"
|
||||
@@ -44,16 +45,16 @@
|
||||
<el-divider v-if="isXpackOrEE" class="divider" />
|
||||
|
||||
<div v-if="showNodes()">
|
||||
<el-scrollbar max-height="168px" :noresize="true">
|
||||
<el-scrollbar max-height="288px" :noresize="true">
|
||||
<div
|
||||
class="dropdown-item"
|
||||
@click="changeNode(item.name)"
|
||||
:disabled="item.status !== 'Healthy'"
|
||||
v-for="item in nodeOptions"
|
||||
v-for="item in visibleNodeOptions"
|
||||
:key="item.name"
|
||||
>
|
||||
<SvgIcon class="icon" iconName="p-zhuji" />
|
||||
{{ item.name === 'local' ? globalStore.getMasterAlias() : item.name }}
|
||||
<span class="node-name">{{ displayNodeName(item) }}</span>
|
||||
<el-tooltip
|
||||
v-if="item.status !== 'Healthy' || !item.isBound"
|
||||
:content="item.isBound ? $t('xpack.node.nodeUnhealthy') : $t('xpack.node.nodeUnbind')"
|
||||
@@ -65,16 +66,11 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-if="showMoreNodes" class="dropdown-item more-node-button" @click.stop="openNodeDrawer">
|
||||
<span>{{ $t('tabs.more') }}...</span>
|
||||
<span class="more-node-count">{{ nodeOptions.length - defaultNodeLimit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-if="showNodes() && nodes?.length > 5"
|
||||
suffix-icon="Search"
|
||||
v-model="filter"
|
||||
@input="changeFilter"
|
||||
class="w-full filter-input"
|
||||
size="small"
|
||||
clearable
|
||||
/>
|
||||
<el-divider class="divider" />
|
||||
<div class="dropdown-item" @click="logout">
|
||||
<SvgIcon class="icon" iconName="p-tuichudenglu3" />
|
||||
@@ -82,6 +78,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
<NodeDrawer
|
||||
v-model="nodeDrawerVisible"
|
||||
:nodes="nodeOptions"
|
||||
:master-alias="globalStore.getMasterAlias()"
|
||||
@change="changeNode"
|
||||
@favorite-change="handleFavoriteChange"
|
||||
/>
|
||||
<UserInfo ref="userInfoRef" :currentUser="currentUser" @search="loadCurrentUser()" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,7 +95,7 @@ import { countExecutingTask } from '@/api/modules/log';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { getAgentSettingInfo } from '@/api/modules/setting';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import bus from '@/global/bus';
|
||||
import { logOutApi } from '@/api/modules/auth';
|
||||
import router from '@/routers';
|
||||
@@ -102,9 +105,9 @@ import { changeToLocal, listNodes, setDefaultNodeInfo } from '@/utils/node';
|
||||
import { Login } from '@/api/interface/auth';
|
||||
import { syncAuthInfo } from '@/utils/rbac';
|
||||
import UserInfo from './user-info/index.vue';
|
||||
import NodeDrawer from './node-drawer/index.vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const filter = ref();
|
||||
const currentUser = ref<Login.AuthInfo>();
|
||||
const { globalStore, currentNode, currentNodeAddr, defaultNetwork, entrance, isEnterprise, isXpackOrEE } =
|
||||
useGlobalStore();
|
||||
@@ -113,11 +116,15 @@ const nodes = ref([]);
|
||||
const nodeOptions = ref([]);
|
||||
const loading = ref();
|
||||
const switchingNode = ref(false);
|
||||
const popoverVisible = ref(false);
|
||||
const nodeDrawerVisible = ref(false);
|
||||
const userInfoRef = ref();
|
||||
const props = defineProps({
|
||||
version: String,
|
||||
});
|
||||
|
||||
const defaultNodeLimit = 8;
|
||||
|
||||
const emit = defineEmits(['openTask', 'refresh']);
|
||||
bus.on('refreshTask', () => {
|
||||
checkTask();
|
||||
@@ -133,19 +140,28 @@ const loadCurrentName = () => {
|
||||
return globalStore.getMasterAlias();
|
||||
};
|
||||
|
||||
const visibleNodeOptions = computed(() => {
|
||||
return nodeOptions.value.slice(0, defaultNodeLimit);
|
||||
});
|
||||
const showMoreNodes = computed(() => {
|
||||
return nodeOptions.value.length > defaultNodeLimit;
|
||||
});
|
||||
|
||||
const showPopover = async () => {
|
||||
filter.value = '';
|
||||
await loadNodes();
|
||||
changeFilter();
|
||||
};
|
||||
|
||||
const changeFilter = () => {
|
||||
nodeOptions.value = [];
|
||||
for (const item of nodes.value) {
|
||||
if (item.name.indexOf(filter.value) !== -1) {
|
||||
nodeOptions.value.push(item);
|
||||
}
|
||||
}
|
||||
const displayNodeName = (item) => {
|
||||
return item.name === 'local' ? globalStore.getMasterAlias() : item.name;
|
||||
};
|
||||
|
||||
const openNodeDrawer = () => {
|
||||
nodeDrawerVisible.value = true;
|
||||
popoverVisible.value = false;
|
||||
};
|
||||
|
||||
const handleFavoriteChange = async () => {
|
||||
await loadNodes();
|
||||
};
|
||||
|
||||
const loadNodes = async () => {
|
||||
@@ -162,11 +178,6 @@ const loadNodes = async () => {
|
||||
if (nodes.value.length === 0) {
|
||||
setDefaultNodeInfo();
|
||||
}
|
||||
nodes.value.sort((a, b) => {
|
||||
if (a.name === 'local') return -1;
|
||||
if (b.name === 'local') return 1;
|
||||
return 0;
|
||||
});
|
||||
nodeOptions.value = nodes.value || [];
|
||||
loading.value = false;
|
||||
})
|
||||
@@ -362,9 +373,28 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.filter-input {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 4px;
|
||||
.node-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.more-node-button {
|
||||
padding-left: 34px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.more-node-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 9px;
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
line-height: 18px;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background: var(--el-menu-item-bg-color-active);
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('setting.selectNode')" @close="handleClose">
|
||||
<template #content>
|
||||
<div class="node-drawer">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
:placeholder="$t('xpack.node.searchNodePlaceholder')"
|
||||
class="node-search-input"
|
||||
clearable
|
||||
/>
|
||||
<div class="node-grid">
|
||||
<section v-for="group in nodeGroups" :key="group.key" class="node-section">
|
||||
<div class="node-table">
|
||||
<div class="node-table__header">
|
||||
<div class="node-table__cell node-table__cell--node">
|
||||
<span class="node-table__group-title">{{ group.label }}</span>
|
||||
<span class="node-table__summary">{{ group.items.length }}</span>
|
||||
</div>
|
||||
<div class="node-table__cell node-table__cell--addr">{{ $t('home.ip') }}</div>
|
||||
</div>
|
||||
<div v-for="row in group.items" :key="row.name" class="node-table__row">
|
||||
<div class="node-table__cell node-table__cell--node">
|
||||
<el-tooltip
|
||||
:content="
|
||||
row.isFavorite ? $t('website.cancelFavorite') : $t('website.favorite')
|
||||
"
|
||||
placement="left"
|
||||
>
|
||||
<el-button
|
||||
class="node-favorite-button"
|
||||
link
|
||||
v-if="isAdmin"
|
||||
:type="row.isFavorite ? 'warning' : 'info'"
|
||||
:icon="row.isFavorite ? 'StarFilled' : 'Star'"
|
||||
:loading="favoriteLoadingIDs.includes(row.id)"
|
||||
@click.stop="toggleFavorite(row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<div class="node-name-cell">
|
||||
<SvgIcon class="icon" iconName="p-zhuji" />
|
||||
<el-button
|
||||
class="node-name-button"
|
||||
link
|
||||
type="primary"
|
||||
@click="selectNode(row.name)"
|
||||
>
|
||||
{{ displayNodeName(row) }}
|
||||
</el-button>
|
||||
<el-tooltip
|
||||
v-if="row.status !== 'Healthy' || !row.isBound"
|
||||
:content="
|
||||
row.isBound
|
||||
? $t('xpack.node.nodeUnhealthy')
|
||||
: $t('xpack.node.nodeUnbind')
|
||||
"
|
||||
placement="left"
|
||||
>
|
||||
<span class="node-status-wrap">
|
||||
<el-icon class="icon-status" type="danger">
|
||||
<Warning />
|
||||
</el-icon>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-table__cell node-table__cell--addr">
|
||||
<span class="node-table__addr-text">{{ row.addr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Setting } from '@/api/interface/setting';
|
||||
import { updateNodeFavorite } from '@/api/modules/setting';
|
||||
import DrawerPro from '@/components/drawer-pro/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const { isAdmin } = useGlobalStore();
|
||||
|
||||
type NodeItem = Setting.NodeItem & {
|
||||
group?: string;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
nodes: NodeItem[];
|
||||
masterAlias: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void;
|
||||
(event: 'change', value: string): void;
|
||||
(event: 'favorite-change'): void;
|
||||
}>();
|
||||
|
||||
const keyword = ref('');
|
||||
const favoriteLoadingIDs = ref<number[]>([]);
|
||||
|
||||
const drawerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const filteredNodes = computed(() => {
|
||||
const searchValue = keyword.value.trim().toLowerCase();
|
||||
return props.nodes.filter((item) => {
|
||||
const group = displayGroupName(item);
|
||||
if (!searchValue) {
|
||||
return true;
|
||||
}
|
||||
return [item.name, group, item.addr].some((value) =>
|
||||
String(value || '')
|
||||
.toLowerCase()
|
||||
.includes(searchValue),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const sortedNodes = computed(() => {
|
||||
return [...filteredNodes.value];
|
||||
});
|
||||
|
||||
const commonNodeLimit = 8;
|
||||
|
||||
const commonNodes = computed(() => {
|
||||
return sortedNodes.value.slice(0, commonNodeLimit);
|
||||
});
|
||||
|
||||
const nodeGroups = computed(() => {
|
||||
const groupMap = new Map<string, { key: string; label: string; items: NodeItem[] }>();
|
||||
const commonNodeIDs = new Set(commonNodes.value.map((item) => item.id));
|
||||
for (const item of sortedNodes.value.filter((item) => !commonNodeIDs.has(item.id))) {
|
||||
const label = displayGroupName(item);
|
||||
if (!groupMap.has(label)) {
|
||||
groupMap.set(label, {
|
||||
key: label,
|
||||
label,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
groupMap.get(label)!.items.push(item);
|
||||
}
|
||||
const groups = Array.from(groupMap.values()).sort((a, b) => a.label.localeCompare(b.label));
|
||||
if (commonNodes.value.length > 0) {
|
||||
groups.unshift({
|
||||
key: 'common',
|
||||
label: i18n.global.t('xpack.node.commonNodes'),
|
||||
items: commonNodes.value,
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
const displayNodeName = (item: NodeItem) => {
|
||||
return item.name === 'local' ? props.masterAlias : item.name;
|
||||
};
|
||||
|
||||
const displayGroupName = (item: NodeItem) => {
|
||||
if (item.groupBelong === 'Default') {
|
||||
return i18n.global.t('commons.table.default');
|
||||
}
|
||||
return item.group || item.groupBelong || i18n.global.t('commons.table.default');
|
||||
};
|
||||
|
||||
const selectNode = (name: string) => {
|
||||
emit('change', name);
|
||||
emit('update:modelValue', false);
|
||||
};
|
||||
|
||||
const toggleFavorite = async (row: NodeItem) => {
|
||||
if (favoriteLoadingIDs.value.includes(row.id)) {
|
||||
return;
|
||||
}
|
||||
favoriteLoadingIDs.value.push(row.id);
|
||||
try {
|
||||
await updateNodeFavorite(row.id, !row.isFavorite);
|
||||
emit('favorite-change');
|
||||
} finally {
|
||||
favoriteLoadingIDs.value = favoriteLoadingIDs.value.filter((id) => id !== row.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value) {
|
||||
keyword.value = '';
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-drawer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: calc(100vh - 104px);
|
||||
padding: 0 16px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.node-search-input {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.node-section {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.node-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.node-table__header,
|
||||
.node-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(140px, 42%);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.node-table__header {
|
||||
min-height: 30px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-table__row {
|
||||
min-height: 34px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-table__cell {
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.node-table__cell--node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.node-table__cell--addr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-table__group-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.node-table__summary {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.node-table__addr-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.node-favorite-button {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.node-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.node-name-button {
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.node-status-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.icon-status {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.node-table__header,
|
||||
.node-table__row {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(96px, 34%);
|
||||
}
|
||||
|
||||
.node-table__cell {
|
||||
padding: 5px 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user