Files
fe.ems.vue3/src/views/traceManage/tshark/index.vue
2025-04-22 14:21:20 +08:00

259 lines
7.5 KiB
Vue

<script setup lang="ts">
import { message, Upload } from 'ant-design-vue/es';
import { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
import { FileType } from 'ant-design-vue/es/upload/interface';
import { PageContainer } from 'antdv-pro-layout';
import DissectionTree from './components/DissectionTree.vue';
import DissectionDump from './components/DissectionDump.vue';
import PacketTable from './components/PacketTable.vue';
import { usePCAP, NO_SELECTION } from './hooks/usePCAP';
import { parseSizeFromFile } from '@/utils/parse-utils';
import { parseDateToStr } from '@/utils/date-utils';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const {
state,
handleSelectedTree,
handleSelectedFindSelection,
handleSelectedFrame,
handleScrollBottom,
handleFilterFrames,
handleLoadExample,
handleLoadFile,
} = usePCAP();
/**上传前检查或转换压缩 */
function fnBeforeUpload(file: FileType) {
const fileName = file.name;
const suff = fileName.substring(fileName.lastIndexOf('.'));
const allowList = ['.pcap', '.cap', '.pcapng', '.pcap0'];
if (!allowList.includes(suff)) {
const msg = `${t('components.UploadModal.onlyAllow')} ${allowList.join(
','
)}`;
message.error(msg, 3);
return Upload.LIST_IGNORE;
}
return true;
}
/**表单上传文件 */
function fnUpload(up: UploadRequestOption) {
handleLoadFile(up.file as File);
}
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:loading="!state.initialized"
:body-style="{ padding: '12px' }"
>
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-upload
name="file"
list-type="picture"
:max-count="1"
accept=".pcap,.cap,.pcapng,.pcap0"
:show-upload-list="false"
:before-upload="fnBeforeUpload"
:custom-request="fnUpload"
>
<a-button type="primary"> Upload </a-button>
</a-upload>
<a-button @click="handleLoadExample()">Example</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tag color="green" v-show="!!state.currentFilter">
{{ state.currentFilter }}
</a-tag>
<span> Matched Frame: {{ state.totalFrames }} </span>
<!-- 包信息 -->
<a-popover
trigger="click"
placement="bottomLeft"
v-if="state.summary.filename"
>
<template #content>
<div class="summary">
<div class="summary-item">
<span>Type:</span>
<span>{{ state.summary.file_type }}</span>
</div>
<div class="summary-item">
<span>Size:</span>
<span>{{
parseSizeFromFile(state.summary.file_length)
}}</span>
</div>
<div class="summary-item">
<span>Encapsulation:</span>
<span>{{ state.summary.file_encap_type }}</span>
</div>
<div class="summary-item">
<span>Packets:</span>
<span>{{ state.summary.packet_count }}</span>
</div>
<div class="summary-item">
<span>Start Time:</span>
<span>
{{
parseDateToStr(
state.summary.start_time * 1000,
'YYYY-MM-DD HH:mm:ss.SSS'
)
}}
</span>
</div>
<div class="summary-item">
<span>Stop Time:</span>
<span>
{{
parseDateToStr(
state.summary.stop_time * 1000,
'YYYY-MM-DD HH:mm:ss.SSS'
)
}}
</span>
</div>
<div class="summary-item">
<span>Duration:</span>
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
</div>
</div>
</template>
<InfoCircleOutlined />
</a-popover>
</a-space>
</template>
<!-- 包数据表过滤 -->
<a-input-group compact>
<a-auto-complete
v-model:value="state.filter"
:options="[
{ value: 'http || tcp.port == 33030 || http2' },
{ value: 'ip.src== 172.17.0.19 && ip.dst == 172.17.0.77' },
{ value: 'sip || ngap' },
]"
style="width: calc(100% - 100px)"
:allow-clear="true"
>
<a-input
placeholder="display filter, example: tcp"
@pressEnter="handleFilterFrames"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
</a-auto-complete>
<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.packetFrames"
:selectedFrame="state.selectedFrame"
:onSelectedFrame="handleSelectedFrame"
:onScrollBottom="handleScrollBottom"
></PacketTable>
<a-row
:gutter="16"
style="border: 2px rgb(217 217 217) solid; border-radius: 6px"
v-show="state.selectedFrame > 0"
>
<a-col :lg="12" :md="12" :xs="24" class="tree">
<!-- 帧数据 -->
<DissectionTree
id="root"
:select="handleSelectedTree"
: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>
</PageContainer>
</template>
<style scoped>
.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>