120 lines
3.8 KiB
Vue
120 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import api from '@/api/client'
|
|
import PageHeader from '@/components/PageHeader.vue'
|
|
import AppCard from '@/components/AppCard.vue'
|
|
|
|
const { t } = useI18n()
|
|
|
|
interface TrafficLog {
|
|
id: number
|
|
host: string
|
|
network: string
|
|
rule_line: string
|
|
outbound_node: string
|
|
outbound_chain: string
|
|
upload: number
|
|
download: number
|
|
success: boolean
|
|
started_at: string
|
|
ended_at: string
|
|
}
|
|
|
|
const items = ref<TrafficLog[]>([])
|
|
const total = ref(0)
|
|
const loading = ref(false)
|
|
const keyword = ref('')
|
|
const page = ref(1)
|
|
const pageSize = ref(50)
|
|
|
|
const successCount = computed(() => items.value.filter((i) => i.success).length)
|
|
|
|
function formatBytes(n: number) {
|
|
if (n < 1024) return `${n} B`
|
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
return `${(n / 1024 / 1024).toFixed(2)} MB`
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const { data } = await api.get('/traffic-logs', {
|
|
params: {
|
|
keyword: keyword.value,
|
|
limit: pageSize.value,
|
|
offset: (page.value - 1) * pageSize.value,
|
|
},
|
|
})
|
|
items.value = data.items || []
|
|
total.value = data.total || 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page">
|
|
<PageHeader :title="t('trafficLogs.title')" :description="t('trafficLogs.desc')">
|
|
<template #actions>
|
|
<el-button @click="load">{{ t('common.refresh') }}</el-button>
|
|
</template>
|
|
</PageHeader>
|
|
|
|
<AppCard>
|
|
<div class="card-toolbar">
|
|
<el-input
|
|
v-model="keyword"
|
|
:placeholder="t('trafficLogs.searchPlaceholder')"
|
|
clearable
|
|
class="toolbar-input--wide"
|
|
@keyup.enter="() => { page = 1; load() }"
|
|
/>
|
|
<el-button type="primary" @click="() => { page = 1; load() }">{{ t('common.search') }}</el-button>
|
|
<span v-if="items.length" class="form-hint form-hint--flush">
|
|
{{ t('trafficLogs.pageSuccess', { ok: successCount, total: items.length }) }}
|
|
</span>
|
|
</div>
|
|
|
|
<el-table :data="items" v-loading="loading" size="small" class="table-scroll--lg">
|
|
<el-table-column prop="started_at" :label="t('common.time')" width="168" />
|
|
<el-table-column prop="host" :label="t('trafficLogs.target')" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="rule_line" :label="t('trafficLogs.matchedRule')" min-width="180" show-overflow-tooltip />
|
|
<el-table-column prop="outbound_node" :label="t('trafficLogs.outboundNode')" min-width="140" show-overflow-tooltip />
|
|
<el-table-column :label="t('trafficLogs.chain')" min-width="160" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
{{ row.outbound_chain || t('common.none') }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('trafficLogs.traffic')" width="120">
|
|
<template #default="{ row }">
|
|
↑{{ formatBytes(row.upload) }} / ↓{{ formatBytes(row.download) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('trafficLogs.result')" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
|
|
{{ row.success ? t('common.success') : t('common.failed') }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div class="card-pager">
|
|
<el-pagination
|
|
v-model:current-page="page"
|
|
v-model:page-size="pageSize"
|
|
:page-sizes="[50, 100, 200, 500]"
|
|
:total="total"
|
|
layout="total, sizes, prev, pager, next"
|
|
@current-change="load"
|
|
@size-change="() => { page = 1; load() }"
|
|
/>
|
|
</div>
|
|
</AppCard>
|
|
</div>
|
|
</template>
|