<script setup lang="ts">
/**
* Nodes/Node/Index — 节点列表(自定义è¿ç§»é¡µï¼‰
*
* MVC æº NodeController.cs + _List_Toolbar_Batch.cshtml 还原:
* - 列表列:ID/ProjectName/Name/Code/Category/ProductCode/CityName/Enable/Version/
* OSKind/Runtime/Framework/IP/OS/MachineName/Cpu/Memory/TotalSize/Logins/
* LastActive/OnlineTime/UpdateTime/UpdateIP
* - Name → 链接 #/Nodes/Node/Detail?id=<ID>(Url å—æ®µï¼Œè‡ªå»ºï¼‰
* - ProjectName → 链接 #/Platform/GalaxyProject?projectId=<ProjectId>(Url å—æ®µï¼Œè‡ªå»ºï¼‰
* - Log → 链接 /Admin/Log?category=节点&linkId=<ID>(Url å—æ®µï¼Œè‡ªå»ºï¼‰
* - 批é‡å·¥å…·æ :选ä¸è¡ŒåŽå¯ç”¨ã€Œå¯ç”¨å‘Šè¦/é‡ç½®å‘Šè¦/ä¿®å¤ã€æŒ‰é’®
* -> GET /Nodes/Node/<Action>?keys=<选ä¸id 逗å·ä¸²>(åŽç«¯è¯» SelectKeys)
* - ã€Œè¿½è¸ªã€æ“作:æ¯è¡ŒæŒ‰é’® → confirm → GET /Nodes/Node/Trace?id=<行Id> → ElMessage「已å‘é€å‘½ä»¤ã€â†’ 刷新
* - æœç´¢ï¼šå¯ç”¨ + å…³é”®å— Q + æ—¶é—´èŒƒå›´ï¼ˆåŠ åˆ†é¡¹ï¼‰
*
* å‰ç«¯ç» usePageApi + request,自定义 Action å†™ç›¸å¯¹æ— å‰ç¼€è·¯å¾„ /Nodes/Node/<Action>
* (request 会自动拼 baseURL,ä¸è¦å†™ /api/)。
*/
import { ref, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import request from '@newlifex/cube-vue/core/utils/request';
import { usePageApi } from '@newlifex/cube-vue/core/composables/useCubeApi';
const api = usePageApi('Nodes', 'Node');
const route = useRoute();
const projectId = ref<number | undefined>(undefined);
interface Row {
Id?: number;
ProjectId?: number;
ProjectName?: string;
Name?: string;
Code?: string;
Category?: string;
ProductCode?: string;
CityName?: string;
Enable?: boolean;
Version?: string;
OSKind?: number | string;
Runtime?: string;
Framework?: string;
IP?: string;
OS?: string;
MachineName?: string;
Cpu?: string;
Memory?: number;
TotalSize?: number;
Logins?: number;
LastActive?: string;
OnlineTime?: number;
UpdateTime?: string;
UpdateIP?: string;
[k: string]: unknown;
}
/** æ ‡å‡†åˆ—ï¼ˆå« Name/ProjectName 链接列在模æ¿è‡ªå»ºæ¸²æŸ“) */
type Col = { name: string; label: string; width?: number; minWidth?: number };
const ALL_COLUMNS: Col[] = [
{ name: 'Id', label: 'ç¼–å·', width: 80 },
{ name: 'ProjectName', label: '项目', minWidth: 130 },
{ name: 'Name', label: 'åç§°', minWidth: 130 },
{ name: 'Code', label: 'ç¼–ç ', minWidth: 120 },
{ name: 'Category', label: '类别', width: 90 },
{ name: 'ProductCode', label: '产å“', minWidth: 110 },
{ name: 'CityName', label: '城市', width: 90 },
{ name: 'Enable', label: 'å¯ç”¨', width: 80 },
{ name: 'Version', label: '版本', width: 110 },
{ name: 'OSKind', label: '系统', width: 90 },
{ name: 'Runtime', label: 'è¿è¡Œæ—¶', width: 110 },
{ name: 'Framework', label: '框架', width: 110 },
{ name: 'IP', label: 'IP', width: 130 },
{ name: 'OS', label: 'æ“作系统', minWidth: 140 },
{ name: 'MachineName', label: '机器å', minWidth: 120 },
{ name: 'Cpu', label: 'CPU', minWidth: 110 },
{ name: 'Memory', label: '内å˜', width: 90 },
{ name: 'TotalSize', label: 'ç£ç›˜', width: 90 },
{ name: 'Logins', label: '登录数', width: 80 },
{ name: 'LastActive', label: 'æœ€åŽæ´»è·ƒ', minWidth: 170 },
{ name: 'OnlineTime', label: '在线时长', width: 110 },
{ name: 'Log', label: '日志', minWidth: 140 },
{ name: 'UpdateTime', label: 'æ›´æ–°æ—¶é—´', minWidth: 170 },
{ name: 'UpdateIP', label: 'æ›´æ–°IP', width: 120 },
];
const BATCH_ACTIONS = [
{ act: 'SetAlarm', label: 'å¯ç”¨å‘Šè¦', params: { enable: 'true' } },
{ act: 'SetAlarm', label: 'ç¦ç”¨å‘Šè¦', params: { enable: 'false' } },
{ act: 'ResetAlarm', label: 'é‡ç½®å‘Šè¦', params: {} },
{ act: 'Fix', label: 'ä¿®å¤', params: {} },
] as const;
/** æœç´¢ï¼šç±»åˆ«/产å“/版本/系统 下拉 + å¯ç”¨ + å…³é”®å— + 时间范围 */
const enableFilter = ref<string>('');
const keyword = ref<string>('');
const timeRange = ref<[string, string] | null>(null);
const categoryOptions = ref<{ value: string; label: string }[]>([]);
const productOptions = ref<{ value: string; label: string }[]>([]);
const versionOptions = ref<{ value: string; label: string }[]>([]);
const osKindOptions = ref<{ value: string; label: string }[]>([]);
const loading = ref(false);
/** åŽç«¯è¿”回 camelCase å—æ®µï¼Œè½¬ PascalCase ä»¥åŒ¹é… Row 接å£ä¸Žæ¨¡æ¿ï¼ˆä¿®å¤å•å…ƒæ ¼å…¨ç©ºé—®é¢˜ï¼‰ */
function toPascalRow(r: Record<string, unknown>): Row {
const out: Record<string, unknown> = {};
for (const k of Object.keys(r)) {
out[k.charAt(0).toUpperCase() + k.slice(1)] = r[k];
}
return out as Row;
}
const rows = ref<Row[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const selectedRows = ref<Row[]>([]);
const batchActing = ref(false);
const hasActiveFilter = computed(
() =>
enableFilter.value !== '' ||
keyword.value.trim() !== '' ||
timeRange.value != null ||
categoryFilter.value !== '' ||
productFilter.value !== '' ||
versionFilter.value !== '' ||
osKindFilter.value !== '',
);
/** 类别/产å“/版本/系统 下拉(MVC Node.FindAllCategory/FindAllProduct/FindAllVersion/FindAllOSKind) */
const categoryFilter = ref<string>('');
const productFilter = ref<string>('');
const versionFilter = ref<string>('');
const osKindFilter = ref<string>('');
const selectedIds = computed(() =>
selectedRows.value.map((r) => r.Id).filter((id): id is number => id != null),
);
/** 内å˜å±•示(MB → GB å‹å¥½ï¼‰ */
function memoryText(row: Row): string {
const v = row.Memory;
if (v == null) return '';
if (v >= 1024) return `${(v / 1024).toFixed(1)} GB`;
return `${v} MB`;
}
/** 在线时长(秒 → å‹å¥½ï¼‰ */
function onlineText(row: Row): string {
const v = row.OnlineTime;
if (v == null) return '';
const s = Number(v);
if (s < 60) return `${s} ç§’`;
if (s < 3600) return `${Math.floor(s / 60)} 分`;
if (s < 86400) return `${(s / 3600).toFixed(1)} æ—¶`;
return `${(s / 86400).toFixed(1)} 天`;
}
function cellText(row: Row, key: string): string {
const v = row[key];
if (v == null) return '';
if (key === 'Enable') return v ? '是' : 'å¦';
if (key === 'Id' || key === 'Logins') return String(v);
if (key === 'Memory') return memoryText(row);
if (key === 'OnlineTime') return onlineText(row);
if (key === 'LastActive' || key === 'UpdateTime')
return String(v).replace('T', ' ').slice(0, 19);
return String(v);
}
/** é“¾æŽ¥ç›®æ ‡ */
function detailUrl(row: Row): string {
return `#/Nodes/Node/Detail?id=${row.Id}`;
}
function projectUrl(row: Row): string {
return `#/Platform/GalaxyProject?projectId=${row.ProjectId}`;
}
function logUrl(row: Row): string {
return `#/Admin/Log?category=节点&linkId=${row.Id}`;
}
function buildParams(): Record<string, unknown> {
const p: Record<string, unknown> = {
pageIndex: page.value - 1,
pageSize: pageSize.value,
};
// 项目维度隔离:从 GalaxyProject 项目页进入时æºå¸¦ projectId,列表按项目过滤
if (projectId.value != null) p.projectId = projectId.value;
if (enableFilter.value !== '') p.enable = enableFilter.value === 'true';
if (keyword.value.trim() !== '') p.Q = keyword.value.trim();
if (categoryFilter.value) p.category = categoryFilter.value;
if (productFilter.value) p.product = productFilter.value;
if (versionFilter.value) p.version = versionFilter.value;
if (osKindFilter.value) p.osKind = osKindFilter.value;
if (timeRange.value?.[0]) p.dtStart = timeRange.value[0];
if (timeRange.value?.[1]) p.dtEnd = timeRange.value[1];
return p;
}
/** ä»Žå·²æœ‰è®°å½•åŽ»é‡æå–类别/产å“/版本/ç³»ç»Ÿä¸‹æ‹‰é€‰é¡¹ï¼Œå¯¹é½ MVC FindAll* */
async function fetchSelectOptions() {
try {
const res: any = await api.getList<Row>({
pageIndex: 0,
pageSize: 1000,
fields: 'Category,Product,Version,OSKind',
});
const data = (res?.data ?? []).map(toPascalRow);
const pick = (key: string) => {
const set = new Set<string>();
data.forEach((r: Row) => {
const v = r[key as keyof Row];
if (v != null && v !== '') set.add(String(v));
});
return Array.from(set)
.sort((a, b) => a.localeCompare(b))
.map((c) => ({ value: c, label: c }));
};
categoryOptions.value = [{ value: '', label: '全部' }, ...pick('Category')];
productOptions.value = [{ value: '', label: '全部' }, ...pick('Product')];
versionOptions.value = [{ value: '', label: '全部' }, ...pick('Version')];
osKindOptions.value = [{ value: '', label: '全部' }, ...pick('OSKind')];
} catch (e) {
console.error('[Node] fetchSelectOptions failed', e);
categoryOptions.value = [{ value: '', label: '全部' }];
productOptions.value = [{ value: '', label: '全部' }];
versionOptions.value = [{ value: '', label: '全部' }];
osKindOptions.value = [{ value: '', label: '全部' }];
}
}
async function fetchList() {
loading.value = true;
try {
const res: any = await api.getList<Row>(buildParams());
rows.value = (res?.data ?? []).map(toPascalRow);
total.value = res?.page?.totalCount ?? res?.page?.total ?? rows.value.length;
} catch (e) {
console.error('[Node] GetList failed', e);
rows.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
page.value = 1;
fetchList();
}
function handleReset() {
enableFilter.value = '';
keyword.value = '';
categoryFilter.value = '';
productFilter.value = '';
versionFilter.value = '';
osKindFilter.value = '';
timeRange.value = null;
page.value = 1;
fetchList();
}
function onSelectionChange(val: Row[]) {
selectedRows.value = val;
}
function onPageChange(p: number) {
page.value = p;
fetchList();
}
/** 阈值输入(MVC _List_Toolbar_Batch 的 alarmRate,默认 80) */
const alarmRate = ref<number>(80);
/** æ‰¹é‡æ“作:GET /Nodes/Node/<Action>?keys=id1,id2&enable/alarmRate */
async function onBatchAction(act: string, params: Record<string, unknown> = {}) {
if (selectedIds.value.length === 0) {
ElMessage.warning('è¯·å…ˆé€‰æ‹©è¦æ“作的节点');
return;
}
try {
await ElMessageBox.confirm(
`确认对选ä¸çš„ ${selectedIds.value.length} 个节点执行「${act}ã€ï¼Ÿ`,
'æ‰¹é‡æ“作',
{ type: 'warning', confirmButtonText: '确认', cancelButtonText: 'å–æ¶ˆ' },
);
} catch {
return;
}
batchActing.value = true;
try {
const reqParams: Record<string, unknown> = { keys: selectedIds.value.join(','), ...params };
if (act === 'ResetAlarm') reqParams.alarmRate = alarmRate.value;
const res: any = await request({
url: `/Nodes/Node/${act}`,
method: 'get',
params: reqParams,
});
if (res && (res.code === 0 || res.code === 200 || res.success)) {
ElMessage.success(res.message || 'æ“作æˆåŠŸ');
await fetchList();
} else {
ElMessage.error(res?.message || 'æ“作失败');
}
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || e?.message || 'æ“作失败');
} finally {
batchActing.value = false;
}
}
/** 「追踪ã€ï¼šconfirm → GET /Nodes/Node/Trace?id= → ElMessage「已å‘é€å‘½ä»¤ã€â†’ 刷新 */
async function onTrace(row: Row) {
if (row.Id == null) return;
try {
await ElMessageBox.confirm('确认å‘该节点å‘é€ã€Œæˆªå±/抓日志ã€å‘½ä»¤ï¼Ÿ', '追踪节点', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: 'å–æ¶ˆ',
});
} catch {
return; // ç”¨æˆ·å–æ¶ˆ
}
try {
const res: any = await request({
url: `/Nodes/Node/Trace`,
method: 'get',
params: { id: row.Id },
});
if (res && (res.code === 0 || res.code === 200 || res.success)) {
ElMessage.success(res.message || 'å·²å‘é€å‘½ä»¤');
} else {
ElMessage.error(res?.message || 'å‘é€å¤±è´¥');
}
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || e?.message || 'å‘é€å¤±è´¥');
} finally {
await fetchList();
}
}
onMounted(async () => {
projectId.value = route.query.projectId ? Number(route.query.projectId) : undefined;
await fetchSelectOptions();
await fetchList();
});
// 路由 projectId å˜åŒ–时(在项目页点击ä¸åŒé¡¹ç›®è·³è½¬ï¼‰é‡æ–°è¿‡æ»¤
watch(
() => route.query.projectId,
(val) => {
projectId.value = val ? Number(val) : undefined;
fetchList();
},
);
</script>
<template>
<div class="list-page node-page">
<div class="lp-header">
<h2 class="lph-title">
节点
</h2>
<div class="lp-toolbar">
<el-button
:loading="loading"
@click="fetchList"
>
刷新
</el-button>
</div>
</div>
<div class="lp-card">
<!-- æœç´¢æ :类别/产å“/版本/系统/å¯ç”¨/å…³é”®å— + æ—¶é—´èŒƒå›´ï¼ˆå¯¹é½ MVC _List_Search) -->
<div class="lp-card-header node-search">
<div class="node-search-grid">
<div class="node-search-field">
<label class="node-search-label">类别</label>
<el-select
v-model="categoryFilter"
clearable
placeholder="全部"
class="node-search-input"
>
<el-option
v-for="opt in categoryOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<div class="node-search-field">
<label class="node-search-label">产å“</label>
<el-select
v-model="productFilter"
clearable
placeholder="全部"
class="node-search-input"
>
<el-option
v-for="opt in productOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<div class="node-search-field">
<label class="node-search-label">版本</label>
<el-select
v-model="versionFilter"
clearable
placeholder="全部"
class="node-search-input"
>
<el-option
v-for="opt in versionOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<div class="node-search-field">
<label class="node-search-label">系统</label>
<el-select
v-model="osKindFilter"
clearable
placeholder="全部"
class="node-search-input"
>
<el-option
v-for="opt in osKindOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<div class="node-search-field">
<label class="node-search-label">å¯ç”¨</label>
<el-select
v-model="enableFilter"
clearable
placeholder="全部"
class="node-search-input"
>
<el-option
label="全部"
value=""
/>
<el-option
label="å¯ç”¨"
value="true"
/>
<el-option
label="ç¦ç”¨"
value="false"
/>
</el-select>
</div>
<div class="node-search-field">
<label class="node-search-label">关键å—</label>
<el-input
v-model="keyword"
placeholder="åç§°/ç¼–ç /IP"
class="node-search-input"
clearable
/>
</div>
<div class="node-search-field node-search-field--wide">
<label class="node-search-label">æ—¶é—´</label>
<el-date-picker
v-model="timeRange"
type="daterange"
range-separator="~"
start-placeholder="开始日期"
end-placeholder="ç»“æŸæ—¥æœŸ"
value-format="YYYY-MM-DD"
class="node-search-input node-search-datepicker"
/>
</div>
<div class="node-search-actions">
<el-button
:class="{ 'is-active': hasActiveFilter }"
@click="handleReset"
>
é‡ç½®
</el-button>
<el-button
type="primary"
@click="handleSearch"
>
<el-icon><Search /></el-icon>æœç´¢
</el-button>
</div>
</div>
</div>
<!-- 批é‡å·¥å…·æ ï¼ˆå¯¹é½ MVC _List_Toolbar_Batch:å¯ç”¨å‘Šè¦/ç¦ç”¨å‘Šè¦/é‡ç½®é˜ˆå€¼+alarmRate/ä¿®æ£ï¼‰ -->
<div class="lp-batchbar">
<span class="lp-batchbar-tip">已选 {{ selectedIds.length }} 项</span>
<el-button
v-for="a in BATCH_ACTIONS"
:key="'batch-' + a.act + a.label"
size="small"
:disabled="selectedIds.length === 0 || batchActing"
:loading="batchActing"
@click="onBatchAction(a.act, a.params)"
>
{{ a.label }}
</el-button>
<span class="lp-batchbar-rate">
阈值
<el-input-number
v-model="alarmRate"
:min="0"
:max="100"
size="small"
controls-position="right"
/>
</span>
</div>
<div class="lp-card-body">
<el-table
v-loading="loading"
:data="rows"
class="act-table"
stripe
border
empty-text="æš‚æ— æ•°æ®"
@selection-change="onSelectionChange"
>
<el-table-column
type="selection"
width="46"
/>
<el-table-column
v-for="c in ALL_COLUMNS"
:key="c.name"
:prop="c.name"
:label="c.label"
:width="c.width"
:min-width="c.minWidth"
show-overflow-tooltip
>
<template #default="{ row }">
<!-- Name 链接列 -->
<el-link
v-if="c.name === 'Name' && row.Id != null"
type="primary"
:underline="false"
:href="detailUrl(row)"
target="_blank"
>
{{ cellText(row, c.name) }}
</el-link>
<!-- ProjectName 链接列 -->
<el-link
v-else-if="c.name === 'ProjectName' && row.ProjectId != null"
type="primary"
:underline="false"
:href="projectUrl(row)"
>
{{ cellText(row, c.name) }}
</el-link>
<!-- 其余嗿®µçº¯æ–‡æœ¬ -->
<span v-else>{{ cellText(row, c.name) }}</span>
</template>
</el-table-column>
<!-- 日志链接列 -->
<el-table-column
label="日志"
min-width="80"
align="center"
>
<template #default="{ row }">
<el-link
v-if="row.Id != null"
type="primary"
:underline="false"
:href="logUrl(row)"
target="_blank"
>
日志
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<!-- æ“作列:追踪 / 详情 / 编辑 / åˆ é™¤ -->
<el-table-column
label="æ“作"
min-width="280"
fixed="right"
align="center"
>
<template #default="{ row }">
<div class="node-ops">
<el-button
size="small"
type="primary"
plain
@click="onTrace(row)"
>
追踪
</el-button>
<el-link
type="primary"
:underline="false"
class="node-op"
@click="fetchList"
>
详情
</el-link>
<el-link
type="primary"
:underline="false"
class="node-op"
@click="fetchList"
>
编辑
</el-link>
<el-link
type="danger"
:underline="false"
class="node-op"
@click="fetchList"
>
åˆ é™¤
</el-link>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="lp-card-footer">
<el-pagination
:current-page="page"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next, sizes"
:page-sizes="[10, 20, 50, 100]"
@current-change="onPageChange"
@size-change="
(s: number) => {
pageSize = s;
page = 1;
fetchList();
}
"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.node-page {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.lp-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--el-main-padding, 24px) var(--el-main-padding, 24px) 0;
.lph-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.lp-toolbar {
display: flex;
gap: 8px;
}
}
.lp-card {
flex: 1;
margin: var(--el-main-padding, 24px);
padding: 16px;
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
border-radius: var(--el-border-radius-base);
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
display: flex;
flex-direction: column;
}
.lp-card-header {
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
}
.lp-batchbar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background: var(--el-fill-color-light);
.lp-batchbar-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-right: 4px;
}
.lp-batchbar-rate {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
:deep(.el-input-number) {
width: 92px;
}
}
}
.lp-card-body {
flex: 1;
min-height: 200px;
overflow: auto;
}
.lp-card-footer {
padding: 12px 20px;
border-top: 1px solid var(--el-border-color-light);
}
}
.node-search {
.node-search-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px 16px;
align-items: end;
}
.node-search-field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
&--wide {
grid-column: span 2;
}
}
.node-search-label {
font-size: 12px;
font-weight: 500;
color: var(--el-text-color-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-search-input {
width: 100%;
:deep(.el-input__wrapper),
:deep(.el-select__wrapper),
:deep(.el-range-editor) {
min-height: 36px;
border-radius: var(--el-border-radius-base);
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
box-shadow: none;
transition:
border-color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
}
:deep(.el-input__wrapper.is-focus),
:deep(.el-select__wrapper.is-focused),
:deep(.el-range-editor.is-active) {
background: var(--el-fill-color-blank);
border-color: var(--el-color-primary);
box-shadow:
0 0 0 1px var(--el-color-primary) inset,
0 0 0 3px var(--el-color-primary-light-8);
}
:deep(.el-input__inner),
:deep(.el-select__selected-item) {
font-size: 13px;
color: var(--el-text-color-primary);
}
}
.node-search-datepicker {
:deep(.el-range-editor) {
width: 100%;
}
}
.node-search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-bottom: 0;
}
}
.act-table {
width: 100%;
}
.node-ops {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
.node-op {
font-size: 13px;
}
}
@media (max-width: 900px) {
.node-search .node-search-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 560px) {
.node-search .node-search-grid {
grid-template-columns: 1fr;
}
.node-search .node-search-field--wide {
grid-column: span 1;
}
}
</style>
|