Files
opsx-vpn/frontend/src/views/ClientDevices.vue
T

86 lines
3.5 KiB
Vue

<template>
<el-card class="page-card" shadow="never">
<template #header>
<div class="vpn-header">
<div>
<b>{{ $t('clientDevices.title') }}</b>
<span style="margin-left:8px;color:#909399;font-size:12px">
{{ $t('clientDevices.hint') }}
</span>
</div>
<el-button :icon="Refresh" circle :loading="loading" :aria-label="$t('common.refresh')" @click="load" />
</div>
</template>
<el-alert v-if="loadError" type="error" :closable="false" :title="loadError" class="load-error"><el-button size="small" @click="load">{{ $t('common.refresh') }}</el-button></el-alert>
<el-table :data="devices" size="default" v-loading="loading">
<el-table-column prop="username" :label="$t('common.username')" width="140" />
<el-table-column prop="name" :label="$t('clientDevices.deviceName')" min-width="150" />
<el-table-column prop="platform" :label="$t('clientDevices.platform')" width="100" />
<el-table-column prop="tunnel_name" :label="$t('clientDevices.tunnel')" min-width="140" />
<el-table-column prop="client_address" :label="$t('clientDevices.address')" width="130" />
<el-table-column :label="$t('common.traffic')" width="170" align="right" header-align="right">
<template #default="{ row }">{{ fmtBytes(row.rx_bytes) }} / {{ fmtBytes(row.tx_bytes) }}</template>
</el-table-column>
<el-table-column :label="$t('clientDevices.lastHandshake')" width="170">
<template #default="{ row }">{{ row.last_seen_at ? new Date(row.last_seen_at).toLocaleString() : '-' }}</template>
</el-table-column>
<el-table-column :label="$t('common.status')" width="90">
<template #default="{ row }">
<el-tag :type="row.revoked ? 'info' : 'success'" size="small">
{{ row.revoked ? $t('clientDevices.revoked') : $t('common.enabled') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" width="110" fixed="right">
<template #default="{ row }">
<el-popconfirm v-if="!row.revoked" :title="$t('clientDevices.revokeConfirm')" @confirm="revoke(row)">
<template #reference>
<el-button type="danger" size="small" plain>{{ $t('clientDevices.revoke') }}</el-button>
</template>
</el-popconfirm>
<span v-else style="color:#c0c4cc">—</span>
</template>
</el-table-column>
<template #empty><el-empty :description="$t('clientDevices.empty')" /></template>
</el-table>
</el-card>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import api from '../api'
import { fmtBytes } from '../utils/format'
const { t } = useI18n()
const devices = ref([])
const loading = ref(false)
const loadError = ref('')
async function load() {
loading.value = true
try {
const { data } = await api.get('/client-devices')
devices.value = data.devices || []
loadError.value = ''
} catch (e) {
loadError.value = e.response?.data?.error || t('common.loadFailed')
} finally {
loading.value = false
}
}
async function revoke(row) {
try {
await api.post(`/client-devices/${row.id}/revoke`)
await load()
} catch (e) { ElMessage.error(e.response?.data?.error || t('common.operationFailed')) }
}
onMounted(load)
</script>
<style scoped>.load-error { margin-bottom: var(--space-4); }</style>