86 lines
3.5 KiB
Vue
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>
|