feat: 信令跟踪功能页面
This commit is contained in:
64
src/api/trace/packet.ts
Normal file
64
src/api/trace/packet.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { request } from '@/plugins/http-fetch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信令跟踪网卡设备列表
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function packetDevices() {
|
||||||
|
return request({
|
||||||
|
url: '/trace/packet/devices',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信令跟踪开始
|
||||||
|
* @param data 对象
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function packetStart(data: Record<string, any>) {
|
||||||
|
return request({
|
||||||
|
url: '/trace/packet/start',
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信令跟踪结束
|
||||||
|
* @param data 对象
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function packetStop(taskNo: string) {
|
||||||
|
return request({
|
||||||
|
url: '/trace/packet/stop',
|
||||||
|
method: 'post',
|
||||||
|
data: { taskNo },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信令跟踪过滤
|
||||||
|
* @param data 对象
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function packetFilter(taskNo: string, expr: string) {
|
||||||
|
return request({
|
||||||
|
url: '/trace/packet/filter',
|
||||||
|
method: 'put',
|
||||||
|
data: { taskNo, expr },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信令跟踪续期保活
|
||||||
|
* @param data 对象
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function packetKeep(taskNo: string, duration: number = 120) {
|
||||||
|
return request({
|
||||||
|
url: '/trace/packet/keep-alive',
|
||||||
|
method: 'put',
|
||||||
|
data: { taskNo, duration },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, computed, unref, onUpdated, watchEffect } from 'vue';
|
import { reactive, ref, computed, unref, watchEffect } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**列表高度 */
|
/**列表高度 */
|
||||||
@@ -122,8 +122,6 @@ const onScroll = (e: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onUpdated(() => {});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
clientData.value.forEach((_, index) => {
|
clientData.value.forEach((_, index) => {
|
||||||
const currentIndex = state.start + index;
|
const currentIndex = state.start + index;
|
||||||
@@ -136,10 +134,6 @@ watchEffect(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableState = reactive({
|
|
||||||
selected: false,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -167,13 +161,13 @@ const tableState = reactive({
|
|||||||
item.number === props.selectedFrame
|
item.number === props.selectedFrame
|
||||||
? 'blue'
|
? 'blue'
|
||||||
: item.bg
|
: item.bg
|
||||||
? `#${item.bg.toString(16).padStart(6, '0')}`
|
? `#${Number(item.bg).toString(16).padStart(6, '0')}`
|
||||||
: '',
|
: '',
|
||||||
color:
|
color:
|
||||||
item.number === props.selectedFrame
|
item.number === props.selectedFrame
|
||||||
? 'white'
|
? 'white'
|
||||||
: item.fg
|
: item.fg
|
||||||
? `#${item.fg.toString(16).padStart(6, '0')}`
|
? `#${Number(item.fg).toString(16).padStart(6, '0')}`
|
||||||
: '',
|
: '',
|
||||||
}"
|
}"
|
||||||
@click="onSelectedFrame(item.number)"
|
@click="onSelectedFrame(item.number)"
|
||||||
|
|||||||
@@ -1,16 +1,513 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, onMounted, toRaw } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||||
import { PageContainer } from 'antdv-pro-layout';
|
import { PageContainer } from 'antdv-pro-layout';
|
||||||
|
import { message, Modal } from 'ant-design-vue/lib';
|
||||||
|
import DissectionTree from '../tshark/components/DissectionTree.vue';
|
||||||
|
import DissectionDump from '../tshark/components/DissectionDump.vue';
|
||||||
|
import PacketTable from '../tshark/components/PacketTable.vue';
|
||||||
|
import {
|
||||||
|
RESULT_CODE_ERROR,
|
||||||
|
RESULT_CODE_SUCCESS,
|
||||||
|
} from '@/constants/result-constants';
|
||||||
|
import { filePullTask } from '@/api/trace/task';
|
||||||
|
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||||
|
import useI18n from '@/hooks/useI18n';
|
||||||
|
import saveAs from 'file-saver';
|
||||||
|
import {
|
||||||
|
packetDevices,
|
||||||
|
packetStart,
|
||||||
|
packetStop,
|
||||||
|
packetFilter,
|
||||||
|
} from '@/api/trace/packet';
|
||||||
|
import { s } from 'vite/dist/node/types.d-aGj9QkWt';
|
||||||
|
const ws = new WS();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
onMounted(() => {});
|
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
|
||||||
|
|
||||||
|
type StateType = {
|
||||||
|
/**网卡设备列表 */
|
||||||
|
devices: { id: string; label: string; children: any[] }[];
|
||||||
|
/**初始化 */
|
||||||
|
initialized: boolean;
|
||||||
|
/**任务 */
|
||||||
|
task: {
|
||||||
|
taskNo: string;
|
||||||
|
device: string;
|
||||||
|
filter: string;
|
||||||
|
outputPCAP: boolean;
|
||||||
|
};
|
||||||
|
/**字段 */
|
||||||
|
columns: string[];
|
||||||
|
|
||||||
|
/**过滤条件 */
|
||||||
|
filter: string;
|
||||||
|
/**过滤条件错误信息 */
|
||||||
|
filterError: string | null;
|
||||||
|
|
||||||
|
/**当前选中的帧编号 */
|
||||||
|
selectedFrame: number;
|
||||||
|
/**当前选中的帧数据 */
|
||||||
|
packetFrame: { tree: any[]; data_sources: any[] };
|
||||||
|
/**pcap包帧数据 */
|
||||||
|
packetFrameTreeMap: Map<string, any> | null;
|
||||||
|
/**当前选中的帧数据 */
|
||||||
|
selectedTree: typeof NO_SELECTION;
|
||||||
|
/**选择帧的Dump数据标签 */
|
||||||
|
selectedDataSourceIndex: number;
|
||||||
|
|
||||||
|
/**包总数 */
|
||||||
|
totalPackets: number;
|
||||||
|
/**包数据 */
|
||||||
|
packetList: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive<StateType>({
|
||||||
|
devices: [],
|
||||||
|
initialized: false,
|
||||||
|
task: {
|
||||||
|
taskNo: 'laYlTbq',
|
||||||
|
device: '192.168.5.58',
|
||||||
|
filter: 'tcp and (port 33030 or 8080)',
|
||||||
|
outputPCAP: false,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
'No.',
|
||||||
|
'Time',
|
||||||
|
'Source',
|
||||||
|
'Destination',
|
||||||
|
'Protocol',
|
||||||
|
'Length',
|
||||||
|
'Info',
|
||||||
|
],
|
||||||
|
filter: 'tcp and (port 33030 or 8080)',
|
||||||
|
filterError: null,
|
||||||
|
selectedFrame: 1,
|
||||||
|
/**当前选中的帧数据 */
|
||||||
|
packetFrame: { tree: [], data_sources: [] },
|
||||||
|
packetFrameTreeMap: null, // 注意:Map 需要额外处理
|
||||||
|
selectedTree: NO_SELECTION, // NO_SELECTION 需要定义
|
||||||
|
/**选择帧的Dump数据标签 */
|
||||||
|
selectedDataSourceIndex: 0,
|
||||||
|
// 包数据
|
||||||
|
totalPackets: 0,
|
||||||
|
packetList: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**清除帧数据和报文信息状态 */
|
||||||
|
function fnReset() {
|
||||||
|
state.initialized = false;
|
||||||
|
// 选择帧的数据
|
||||||
|
state.selectedFrame = 0;
|
||||||
|
state.packetFrame = { tree: [], data_sources: [] };
|
||||||
|
state.packetFrameTreeMap = null;
|
||||||
|
state.selectedTree = NO_SELECTION;
|
||||||
|
state.selectedDataSourceIndex = 0;
|
||||||
|
// 过滤条件
|
||||||
|
state.filter = 'tcp and (port 33030 or 8080)';
|
||||||
|
state.filterError = null;
|
||||||
|
// 包数据
|
||||||
|
state.totalPackets = 0;
|
||||||
|
state.packetList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**解析帧数据为简单结构 */
|
||||||
|
function parseFrameTree(id: string, node: Record<string, any>) {
|
||||||
|
let map = new Map();
|
||||||
|
|
||||||
|
if (node.tree && node.tree.length > 0) {
|
||||||
|
for (let i = 0; i < node.tree.length; i++) {
|
||||||
|
const subMap = parseFrameTree(`${id}-${i}`, node.tree[i]);
|
||||||
|
subMap.forEach((value, key) => {
|
||||||
|
map.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (node.length > 0) {
|
||||||
|
map.set(id, {
|
||||||
|
id: id,
|
||||||
|
idx: node.data_source_idx,
|
||||||
|
start: node.start,
|
||||||
|
length: node.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**帧数据点击选中 */
|
||||||
|
function handleSelectedTreeEntry(e: any) {
|
||||||
|
console.log('fnSelectedTreeEntry', e);
|
||||||
|
state.selectedTree = e;
|
||||||
|
}
|
||||||
|
/**报文数据点击选中 */
|
||||||
|
function handleSelectedFindSelection(src_idx: number, pos: number) {
|
||||||
|
console.log('fnSelectedFindSelection', pos);
|
||||||
|
if (state.packetFrameTreeMap == null) return;
|
||||||
|
// find the smallest one
|
||||||
|
let current = null;
|
||||||
|
for (let [k, pp] of state.packetFrameTreeMap) {
|
||||||
|
if (pp.idx !== src_idx) continue;
|
||||||
|
|
||||||
|
if (pos >= pp.start && pos <= pp.start + pp.length) {
|
||||||
|
if (
|
||||||
|
current != null &&
|
||||||
|
state.packetFrameTreeMap.get(current).length > pp.length
|
||||||
|
) {
|
||||||
|
current = k;
|
||||||
|
} else {
|
||||||
|
current = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current != null) {
|
||||||
|
state.selectedTree = state.packetFrameTreeMap.get(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**包数据表点击选中 */
|
||||||
|
function handleSelectedFrame(num: number) {
|
||||||
|
console.log('fnSelectedFrame', num, state.totalPackets);
|
||||||
|
const packet = state.packetList.find((v: any) => v.number === num);
|
||||||
|
if (!packet) return;
|
||||||
|
const packetFrame = packet.frame;
|
||||||
|
state.selectedFrame = packet.number;
|
||||||
|
state.packetFrame = packetFrame;
|
||||||
|
state.packetFrameTreeMap = parseFrameTree('root', packetFrame);
|
||||||
|
state.selectedTree = NO_SELECTION;
|
||||||
|
state.selectedDataSourceIndex = 0;
|
||||||
|
}
|
||||||
|
/**包数据表滚动底部加载 */
|
||||||
|
function handleScrollBottom(index: any) {
|
||||||
|
console.log('handleScrollBottom', index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**开始跟踪 */
|
||||||
|
function fnStart() {
|
||||||
|
// state.task.taskNo = 'laYlTbq';
|
||||||
|
state.task.taskNo = Number(Date.now()).toString(16);
|
||||||
|
state.task.outputPCAP = false;
|
||||||
|
packetStart(state.task).then(res => {
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS) {
|
||||||
|
fnReset();
|
||||||
|
fnWS();
|
||||||
|
} else {
|
||||||
|
message.error(t('common.operateErr'), 3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**停止跟踪 */
|
||||||
|
function fnStop() {
|
||||||
|
packetStop(state.task.taskNo).then(res => {
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS) {
|
||||||
|
ws.close();
|
||||||
|
state.initialized = false;
|
||||||
|
state.filter = '';
|
||||||
|
state.filterError = null;
|
||||||
|
} else {
|
||||||
|
message.warning(res.msg, 3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**跟踪数据表过滤 */
|
||||||
|
function handleFilterFrames() {
|
||||||
|
packetFilter(state.task.taskNo, state.filter).then(res => {
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS) {
|
||||||
|
state.task.filter = state.filter;
|
||||||
|
} else {
|
||||||
|
state.filterError = res.msg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**开始跟踪 */
|
||||||
|
function fnDevice(v: string) {
|
||||||
|
state.task.device = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**下载触发等待 */
|
||||||
|
let downLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
/**信息文件下载 */
|
||||||
|
function fnDownloadPCAP() {
|
||||||
|
if (downLoading.value) return;
|
||||||
|
const fileName = `trace_packet_${state.task.taskNo}.pcap`;
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('common.tipTitle'),
|
||||||
|
content: t('views.logManage.neFile.downTip', { fileName }),
|
||||||
|
onOk() {
|
||||||
|
downLoading.value = true;
|
||||||
|
const hide = message.loading(t('common.loading'), 0);
|
||||||
|
filePullTask(state.task.taskNo)
|
||||||
|
.then(res => {
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS) {
|
||||||
|
message.success({
|
||||||
|
content: t('common.msgSuccess', {
|
||||||
|
msg: t('common.downloadText'),
|
||||||
|
}),
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
saveAs(res.data, `${fileName}`);
|
||||||
|
} else {
|
||||||
|
message.error({
|
||||||
|
content: t('views.logManage.neFile.downTipErr'),
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
hide();
|
||||||
|
downLoading.value = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**接收数据后回调 */
|
||||||
|
function wsMessage(res: Record<string, any>) {
|
||||||
|
const { code, requestId, data } = res;
|
||||||
|
if (code === RESULT_CODE_ERROR) {
|
||||||
|
console.warn(res.msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建联时发送请求
|
||||||
|
if (!requestId && data.clientId) {
|
||||||
|
state.initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅组信息
|
||||||
|
if (!data?.groupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.groupId === `4_${state.task.taskNo}`) {
|
||||||
|
const packetData = data.data;
|
||||||
|
state.totalPackets = packetData.number;
|
||||||
|
state.packetList.push(packetData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**建立WS连接 */
|
||||||
|
function fnWS() {
|
||||||
|
const options: OptionsType = {
|
||||||
|
url: '/ws',
|
||||||
|
params: {
|
||||||
|
/**订阅通道组
|
||||||
|
*
|
||||||
|
* 信令跟踪Packet (GroupID:4_taskNo)
|
||||||
|
*/
|
||||||
|
subGroupID: `4_${state.task.taskNo}`,
|
||||||
|
},
|
||||||
|
onmessage: wsMessage,
|
||||||
|
onerror: (ev: any) => {
|
||||||
|
// 接收数据后回调
|
||||||
|
console.error(ev);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
//建立连接
|
||||||
|
ws.connect(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
packetDevices().then(res => {
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS) {
|
||||||
|
state.devices = res.data;
|
||||||
|
if (res.data.length === 0) return;
|
||||||
|
state.task.device = res.data[0].id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
<a-card :bordered="false" :body-style="{ padding: '12px' }">
|
||||||
<h1>JS</h1>
|
<div class="toolbar">
|
||||||
|
<a-space :size="8" class="toolbar-oper">
|
||||||
|
<a-dropdown-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="state.initialized"
|
||||||
|
@click="fnStart"
|
||||||
|
>
|
||||||
|
<PlayCircleOutlined />
|
||||||
|
Start Trace
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu
|
||||||
|
@click="({ key }:any) => fnDevice(key)"
|
||||||
|
:selectedKeys="[state.task.device]"
|
||||||
|
>
|
||||||
|
<a-menu-item v-for="v in state.devices" :key="v.id">
|
||||||
|
<a-popover placement="rightTop" trigger="hover">
|
||||||
|
<template #content>
|
||||||
|
<div v-for="c in v.children">{{ c.id }}</div>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<span>IP Address</span>
|
||||||
|
</template>
|
||||||
|
<div>{{ v.label }}</div>
|
||||||
|
</a-popover>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
<template #icon><DownOutlined /></template>
|
||||||
|
</a-dropdown-button>
|
||||||
|
|
||||||
|
<a-button danger @click.prevent="fnStop()" v-if="state.initialized">
|
||||||
|
<template #icon><CloseCircleOutlined /></template>
|
||||||
|
Stop Trace
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="downLoading"
|
||||||
|
@click.prevent="fnDownloadPCAP()"
|
||||||
|
>
|
||||||
|
<template #icon><DownloadOutlined /></template>
|
||||||
|
{{ t('common.downloadText') }}
|
||||||
|
</a-button>
|
||||||
|
<a-tag
|
||||||
|
color="green"
|
||||||
|
v-show="!!state.task.filter && state.initialized"
|
||||||
|
>
|
||||||
|
{{ state.task.filter }}
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
|
||||||
|
<a-space :size="8" class="toolbar-info">
|
||||||
|
<span>
|
||||||
|
{{ t('views.traceManage.task.traceId') }}:
|
||||||
|
<strong>{{ state.task.taskNo }}</strong>
|
||||||
|
</span>
|
||||||
|
<span> Packets: {{ state.totalPackets }} </span>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 包数据表过滤 -->
|
||||||
|
<a-input-group compact v-show="state.initialized">
|
||||||
|
<a-input
|
||||||
|
v-model:value="state.filter"
|
||||||
|
placeholder="display filter, example: tcp"
|
||||||
|
:allow-clear="true"
|
||||||
|
style="width: calc(100% - 100px)"
|
||||||
|
@pressEnter="handleFilterFrames"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FilterOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
html-type="submit"
|
||||||
|
style="width: 100px"
|
||||||
|
@click="handleFilterFrames"
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</a-button>
|
||||||
|
</a-input-group>
|
||||||
|
<a-alert
|
||||||
|
:message="state.filterError"
|
||||||
|
type="error"
|
||||||
|
v-if="state.filterError != null"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 包数据表 -->
|
||||||
|
<PacketTable
|
||||||
|
:columns="state.columns"
|
||||||
|
:data="state.packetList"
|
||||||
|
:selectedFrame="state.selectedFrame"
|
||||||
|
:onSelectedFrame="handleSelectedFrame"
|
||||||
|
:onScrollBottom="handleScrollBottom"
|
||||||
|
></PacketTable>
|
||||||
|
|
||||||
|
<a-row :gutter="20">
|
||||||
|
<a-col :lg="12" :md="12" :xs="24" class="tree">
|
||||||
|
<!-- 帧数据 -->
|
||||||
|
<DissectionTree
|
||||||
|
id="root"
|
||||||
|
:select="handleSelectedTreeEntry"
|
||||||
|
:selected="state.selectedTree"
|
||||||
|
:tree="state.packetFrame.tree"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :lg="12" :md="12" :xs="24" class="dump">
|
||||||
|
<!-- 报文数据 -->
|
||||||
|
<a-tabs
|
||||||
|
v-model:activeKey="state.selectedDataSourceIndex"
|
||||||
|
:tab-bar-gutter="16"
|
||||||
|
:tab-bar-style="{ marginBottom: '8px' }"
|
||||||
|
>
|
||||||
|
<a-tab-pane
|
||||||
|
:key="idx"
|
||||||
|
:tab="v.name"
|
||||||
|
v-for="(v, idx) in state.packetFrame.data_sources"
|
||||||
|
style="overflow: auto"
|
||||||
|
>
|
||||||
|
<DissectionDump
|
||||||
|
:base64="v.data"
|
||||||
|
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
|
||||||
|
:selected="
|
||||||
|
idx === state.selectedTree.idx
|
||||||
|
? state.selectedTree
|
||||||
|
: NO_SELECTION
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.toolbar-oper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.toolbar-info {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.summary-item > span:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-y: auto;
|
||||||
|
user-select: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.tree > ul.tree {
|
||||||
|
min-height: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dump {
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.dump .ant-tabs-tabpane {
|
||||||
|
min-height: calc(15rem - 56px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user