feat: 信令抓包tshark解析pcap
This commit is contained in:
141
src/views/traceManage/tshark/components/DissectionDump.vue
Normal file
141
src/views/traceManage/tshark/components/DissectionDump.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import DissectionDumpHigh from './DissectionDumpHigh.vue';
|
||||
|
||||
const props = defineProps({
|
||||
base64: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
select: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
selected: {
|
||||
type: Object,
|
||||
default: { id: '', idx: 0, start: 0, length: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
const addrLines = ref<string[]>([]);
|
||||
const hexLines = ref<string[]>([]);
|
||||
const asciiLines = ref<string[]>([]);
|
||||
|
||||
const asciiHighlight = ref([0, 0]);
|
||||
const hexHighlight = ref([0, 0]);
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
newSelected => {
|
||||
const { start, length: size } = newSelected;
|
||||
|
||||
const hexSize = size * 2 + size - 1;
|
||||
const hexPos = start * 2 + start;
|
||||
const asciiPos = start + Math.floor(start / 16);
|
||||
const asciiSize = start + size + Math.floor((start + size) / 16) - asciiPos;
|
||||
|
||||
asciiHighlight.value = [asciiPos, size > 0 ? asciiSize : 0];
|
||||
hexHighlight.value = [hexPos, size > 0 ? hexSize : 0];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.base64,
|
||||
base64Str => {
|
||||
// Decode base64 to a string
|
||||
const binaryString = atob(base64Str);
|
||||
// Convert binary string to Uint8Array
|
||||
const newBuffer = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
newBuffer[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
let addrLinesTemp: string[] = [];
|
||||
let hexLinesTemp: string[] = [];
|
||||
let asciiLinesTemp: string[] = [];
|
||||
|
||||
for (let i = 0; i < newBuffer.length; i += 16) {
|
||||
let address = i.toString(16).padStart(8, '0');
|
||||
let block = newBuffer.slice(i, i + 16);
|
||||
let hexArray = [];
|
||||
let asciiArray = [];
|
||||
|
||||
for (let value of block) {
|
||||
hexArray.push(value.toString(16).padStart(2, '0'));
|
||||
asciiArray.push(
|
||||
value >= 0x20 && value < 0x7f ? String.fromCharCode(value) : '.'
|
||||
);
|
||||
}
|
||||
|
||||
let hexString =
|
||||
hexArray.length > 8
|
||||
? hexArray.slice(0, 8).join(' ') + ' ' + hexArray.slice(8).join(' ')
|
||||
: hexArray.join(' ');
|
||||
|
||||
let asciiString = asciiArray.join('');
|
||||
|
||||
addrLinesTemp.push(address);
|
||||
hexLinesTemp.push(hexString);
|
||||
asciiLinesTemp.push(asciiString);
|
||||
}
|
||||
|
||||
addrLines.value = addrLinesTemp;
|
||||
hexLines.value = hexLinesTemp;
|
||||
asciiLines.value = asciiLinesTemp;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const onHexClick = (offset: number) => {
|
||||
if (typeof props.select !== 'function') return;
|
||||
props.select(Math.floor(offset / 3));
|
||||
};
|
||||
|
||||
const onAsciiClick = (offset: number) => {
|
||||
if (typeof props.select !== 'function') return;
|
||||
props.select(offset - Math.floor(offset / 17));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tbd">
|
||||
<div class="tbd-offset">
|
||||
{{ addrLines.join('\n') }}
|
||||
</div>
|
||||
<div class="tbd-box">
|
||||
<DissectionDumpHigh
|
||||
:text="hexLines.join('\n')"
|
||||
:start="hexHighlight[0]"
|
||||
:size="hexHighlight[1]"
|
||||
:onOffsetClicked="onHexClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="tbd-box">
|
||||
<DissectionDumpHigh
|
||||
:text="asciiLines.join('\n')"
|
||||
:start="asciiHighlight[0]"
|
||||
:size="asciiHighlight[1]"
|
||||
:onOffsetClicked="onAsciiClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.tbd {
|
||||
display: flex;
|
||||
white-space: pre;
|
||||
word-break: break-all;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
.tbd-offset {
|
||||
color: #6b7280;
|
||||
user-select: none;
|
||||
}
|
||||
.tbd-box {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
start: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
onOffsetClicked: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const before = computed(() => props.text.substring(0, props.start));
|
||||
const hl = computed(() =>
|
||||
props.text.substring(props.start, props.start + props.size)
|
||||
);
|
||||
const end = computed(() => props.text.substring(props.start + props.size));
|
||||
|
||||
const handleClick = (offset: number) => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection) return;
|
||||
props.onOffsetClicked(selection.anchorOffset + offset);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span @click="handleClick(0)">{{ before }}</span>
|
||||
<span @click="handleClick(before.length)" class="hl">
|
||||
{{ hl }}
|
||||
</span>
|
||||
<span @click="handleClick(before.length + hl.length)">
|
||||
{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.hl {
|
||||
color: #ffffff;
|
||||
background-color: #4b5563;
|
||||
}
|
||||
</style>
|
||||
59
src/views/traceManage/tshark/components/DissectionTree.vue
Normal file
59
src/views/traceManage/tshark/components/DissectionTree.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import DissectionTreeSub from './DissectionTreeSub.vue';
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tree: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
sub: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
select: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
selected: {
|
||||
type: Object,
|
||||
default: { id: '', idx: 0, start: 0, length: 0 },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul :class="{ tree: true, 'tree-issub': sub }">
|
||||
<li v-for="(n, i) in tree" :key="`${id}-${i}`" class="tree-li">
|
||||
<DissectionTreeSub
|
||||
:id="`${id}-${i}`"
|
||||
:node="n"
|
||||
:select="select"
|
||||
:selected="selected"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.tree {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid #e5e7eb;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tree-issub {
|
||||
padding-left: 0.5rem;
|
||||
border-left-width: 1px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.tree-li {
|
||||
display: list-item;
|
||||
text-align: -webkit-match-parent;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
116
src/views/traceManage/tshark/components/DissectionTreeSub.vue
Normal file
116
src/views/traceManage/tshark/components/DissectionTreeSub.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import DissectionTree from './DissectionTree.vue';
|
||||
import {
|
||||
CaretDownOutlined,
|
||||
CaretRightOutlined,
|
||||
MinusOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
select: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
selected: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:selected']);
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
() => {
|
||||
if (!open.value) {
|
||||
open.value = props.selected.id.startsWith(props.id + '-');
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const toggle = () => {
|
||||
if (open.value && props.selected.id.startsWith(props.id + '-')) {
|
||||
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
|
||||
emit('update:selected', NO_SELECTION);
|
||||
if (typeof props.select === 'function') {
|
||||
props.select(NO_SELECTION);
|
||||
}
|
||||
}
|
||||
open.value = !open.value;
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (props.node.length > 0) {
|
||||
const select = {
|
||||
id: props.id,
|
||||
idx: props.node.data_source_idx,
|
||||
start: props.node.start,
|
||||
length: props.node.length,
|
||||
};
|
||||
emit('update:selected', select);
|
||||
if (typeof props.select === 'function') {
|
||||
props.select(select);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'tree-sub': true, 'tree-sub_hl': id === selected.id }">
|
||||
<component
|
||||
:is="
|
||||
node.tree && node.tree.length > 0
|
||||
? open
|
||||
? CaretDownOutlined
|
||||
: CaretRightOutlined
|
||||
: MinusOutlined
|
||||
"
|
||||
class="tree-sub_icon"
|
||||
@click="toggle"
|
||||
/>
|
||||
<span @click="handleClick" @dblclick="toggle" class="tree-sub_text">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
<DissectionTree
|
||||
v-if="node.tree && node.tree.length > 0 && open"
|
||||
:id="id"
|
||||
:tree="node.tree"
|
||||
:select="select"
|
||||
:selected="selected"
|
||||
sub
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.tree-sub {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tree-sub_hl {
|
||||
color: #ffffff;
|
||||
background-color: #4b5563;
|
||||
}
|
||||
.tree-sub_icon {
|
||||
color: #6b7280;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.tree-sub_text {
|
||||
width: 100%;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
293
src/views/traceManage/tshark/components/PacketTable.vue
Normal file
293
src/views/traceManage/tshark/components/PacketTable.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, computed, unref, onUpdated, watchEffect } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
/**列表高度 */
|
||||
height: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
/**列表项高度 */
|
||||
itemHeight: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
},
|
||||
/**数据 */
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
/**预先兜底缓存数量 */
|
||||
cache: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
|
||||
/**列 */
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedFrame: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
onSelectedFrame: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onScrollBottom: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive<any>({
|
||||
start: 0,
|
||||
end: 10,
|
||||
scrollOffset: 0,
|
||||
cacheData: [],
|
||||
});
|
||||
const virtualListRef = ref();
|
||||
|
||||
const getWrapperStyle = computed(() => {
|
||||
const { height } = props;
|
||||
return {
|
||||
height: `${height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const getInnerStyle = computed(() => {
|
||||
return {
|
||||
height: `${unref(getTotalHeight)}px`,
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
const getListStyle = computed(() => {
|
||||
return {
|
||||
willChange: 'transform',
|
||||
transform: `translateY(${state.scrollOffset}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
// 数据数量
|
||||
const total = computed(() => {
|
||||
return props.data.length;
|
||||
});
|
||||
|
||||
// 总体高度
|
||||
const getTotalHeight = computed(() => {
|
||||
return unref(total) * props.itemHeight;
|
||||
});
|
||||
|
||||
// 当前屏幕显示的数量
|
||||
const clientCount = computed(() => {
|
||||
return Math.ceil(props.height / props.itemHeight);
|
||||
});
|
||||
|
||||
// 当前屏幕显示的数据
|
||||
const clientData = computed<any[]>(() => {
|
||||
return props.data.slice(state.start, state.end);
|
||||
});
|
||||
|
||||
const onScroll = (e: any) => {
|
||||
const { scrollHeight, scrollTop, clientHeight } = e.target;
|
||||
if (state.scrollOffset === scrollTop) return;
|
||||
const { cache, height, itemHeight } = props;
|
||||
const cacheCount = Math.max(1, cache);
|
||||
|
||||
let startIndex = Math.floor(scrollTop / itemHeight);
|
||||
|
||||
const endIndex = Math.max(
|
||||
0,
|
||||
Math.min(unref(total), startIndex + unref(clientCount) + cacheCount)
|
||||
);
|
||||
|
||||
if (startIndex > cacheCount) {
|
||||
startIndex = startIndex - cacheCount;
|
||||
}
|
||||
|
||||
// 偏移量
|
||||
const offset = scrollTop - (scrollTop % itemHeight);
|
||||
Object.assign(state, {
|
||||
start: startIndex,
|
||||
end: endIndex,
|
||||
scrollOffset: offset,
|
||||
});
|
||||
|
||||
// 底部小于高度时触发
|
||||
if (scrollHeight - scrollTop - clientHeight < height) {
|
||||
props.onScrollBottom(endIndex);
|
||||
}
|
||||
};
|
||||
|
||||
onUpdated(() => {});
|
||||
|
||||
watchEffect(() => {
|
||||
clientData.value.forEach((_, index) => {
|
||||
const currentIndex = state.start + index;
|
||||
if (Object.hasOwn(state.cacheData, currentIndex)) return;
|
||||
state.cacheData[currentIndex] = {
|
||||
top: currentIndex * props.itemHeight,
|
||||
height: props.itemHeight,
|
||||
bottom: (currentIndex + 1) * props.itemHeight,
|
||||
index: currentIndex,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const tableState = reactive({
|
||||
selected: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table">
|
||||
<div class="thead">
|
||||
<div class="thead-item" v-for="v in columns">
|
||||
{{ v }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="virtual-list-wrapper"
|
||||
ref="wrapperRef"
|
||||
:style="getWrapperStyle"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<div class="virtual-list-inner" ref="innerRef" :style="getInnerStyle">
|
||||
<div class="virtual-list" :style="getListStyle" ref="virtualListRef">
|
||||
<div
|
||||
class="tbody"
|
||||
v-for="(item, index) in clientData"
|
||||
:key="index + state.start"
|
||||
:style="{
|
||||
height: itemHeight + 'px',
|
||||
backgroundColor:
|
||||
item.number === props.selectedFrame
|
||||
? 'blue'
|
||||
: item.bg
|
||||
? `#${item.bg.toString(16).padStart(6, '0')}`
|
||||
: '',
|
||||
color:
|
||||
item.number === props.selectedFrame
|
||||
? 'white'
|
||||
: item.fg
|
||||
? `#${item.fg.toString(16).padStart(6, '0')}`
|
||||
: '',
|
||||
}"
|
||||
@click="onSelectedFrame(item.number)"
|
||||
>
|
||||
<div class="tbody-item" v-for="col in item.columns">
|
||||
{{ col }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.virtual-list-wrapper {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.thead {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.thead-item {
|
||||
white-space: nowrap;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 600;
|
||||
/* flex-basis: 100%; */
|
||||
}
|
||||
.tbody {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-top: 1px #f0f0f0 solid;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tbody-item {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
/* flex-basis: 100%; */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.thead-item:nth-child(1),
|
||||
.tbody-item:nth-child(1) {
|
||||
flex-basis: 5rem;
|
||||
width: 5rem;
|
||||
}
|
||||
.tbody-item:nth-child(1) {
|
||||
text-align: right;
|
||||
}
|
||||
.thead-item:nth-child(2),
|
||||
.tbody-item:nth-child(2) {
|
||||
flex-basis: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
.thead-item:nth-child(3),
|
||||
.tbody-item:nth-child(3) {
|
||||
flex-basis: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
.thead-item:nth-child(4),
|
||||
.tbody-item:nth-child(4) {
|
||||
flex-basis: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
.thead-item:nth-child(5),
|
||||
.tbody-item:nth-child(5) {
|
||||
flex-basis: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
.thead-item:nth-child(6),
|
||||
.tbody-item:nth-child(6) {
|
||||
flex-basis: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
.tbody-item:nth-child(6) {
|
||||
text-align: right;
|
||||
}
|
||||
.thead-item:nth-child(7),
|
||||
.tbody-item:nth-child(7) {
|
||||
text-align: left;
|
||||
text-wrap: nowrap;
|
||||
flex: 1;
|
||||
width: 5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 修改滚动条的样式 */
|
||||
.tbody-item:nth-child(7)::-webkit-scrollbar {
|
||||
width: 4px; /* 设置滚动条宽度 */
|
||||
height: 4px;
|
||||
}
|
||||
.tbody-item:nth-child(7)::-webkit-scrollbar-track {
|
||||
background-color: #f0f0f0; /* 设置滚动条轨道背景颜色 */
|
||||
}
|
||||
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb {
|
||||
background-color: #bfbfbf; /* 设置滚动条滑块颜色 */
|
||||
}
|
||||
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #1890ff; /* 设置鼠标悬停时滚动条滑块颜色 */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user