feat(frontend): align console IA with integrations sidebar

Add 对接与扩展 (IPSec, Feishu/OIDC, Plugins, Nodes), move identity
out of settings onto /identity, make sessions/logs tab-deep-linkable,
and rename ops audit / dial-in / admin labels to match the locked nav.
This commit is contained in:
Codex committed 2026-10-23 16:12:02 +08:00
1 parent 7f80f0f7ce
commit f01d3dc5ea
12 files changed
+584 -262

No files matched your search

+75 -14
View File
@@ -1,6 +1,8 @@
<template>
<div>
<el-alert v-if="loadError" type="error" :closable="false" :title="loadError" class="session-error"><el-button size="small" @click="load">{{ $t('common.refresh') }}</el-button></el-alert>
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane :label="$t('sessions.title')" name="online">
<el-alert v-if="sessionError" type="error" :closable="false" :title="sessionError" class="session-error"><el-button size="small" @click="loadOnline">{{ $t('common.refresh') }}</el-button></el-alert>
<!-- 在线会话 -->
<el-card class="page-card" shadow="never" style="margin-bottom:16px">
<template #header>
@@ -9,7 +11,7 @@
<b>{{ $t('sessions.title') }}</b>
<el-tag size="small" type="success" style="margin-left:8px">{{ $t('sessions.onlineCount', { count: sessions.length }) }}</el-tag>
</div>
<el-button link @click="load">{{ $t('common.refresh') }}</el-button>
<el-button link @click="loadOnline">{{ $t('common.refresh') }}</el-button>
</div>
</template>
<el-table :data="sessions" size="default" v-loading="loading">
@@ -59,10 +61,18 @@
</el-table>
</el-card>
</el-tab-pane>
<el-tab-pane :label="$t('sessions.auditLog')" name="audit">
<el-alert v-if="auditError" type="error" :closable="false" :title="auditError" class="session-error"><el-button size="small" @click="loadAudit">{{ $t('common.refresh') }}</el-button></el-alert>
<!-- 连接审计日志 -->
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('sessions.auditLog') }}</b></template>
<el-table :data="logs" size="small" max-height="500">
<template #header>
<div class="row-between">
<b>{{ $t('sessions.auditLog') }}</b>
<el-button link @click="loadAudit">{{ $t('common.refresh') }}</el-button>
</div>
</template>
<el-table :data="logs" size="small" max-height="500" v-loading="auditLoading">
<el-table-column prop="created_at" :label="$t('common.time')" width="170">
<template #default="{ row }">{{ new Date(row.created_at).toLocaleString() }}</template>
</el-table-column>
@@ -78,11 +88,14 @@
<el-table-column prop="detail" :label="$t('common.detail')" show-overflow-tooltip />
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import api from '../api'
@@ -90,11 +103,37 @@ import { fmtBytes } from '../utils/format'
import { protocolColor, protocolDisplay } from '../protocols'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const sessions = ref([])
const logs = ref([])
const alerts = ref([])
const loading = ref(false)
const loadError = ref('')
/** 规范名 online / audit;兼容 ?tab=sessions|connection */
function normalizeTab(name) {
if (name === 'sessions' || name === 'online') return 'online'
if (name === 'audit' || name === 'connection') return 'audit'
return 'online'
}
const activeTab = ref(normalizeTab(route.query.tab))
const sessionError = ref('')
const auditError = ref('')
const auditLoading = ref(false)
watch(() => route.query.tab, (name) => {
const next = normalizeTab(name)
if (activeTab.value !== next) activeTab.value = next
}, { immediate: true })
watch(activeTab, (tab) => {
// 写入规范 query:在线清空;审计固定 tab=audit(别名落地后会改写)
const query = tab === 'online' ? {} : { tab: 'audit' }
const cur = route.query.tab
const needsReplace = normalizeTab(cur) !== tab
|| (tab === 'online' && cur != null && cur !== '')
|| (tab === 'audit' && cur !== 'audit')
if (needsReplace) router.replace({ path: '/sessions', query })
})
function displayUsername(value) {
return typeof value === 'string' && value.trim() && value.trim().toUpperCase() !== 'UNDEF'
@@ -116,34 +155,56 @@ function eventTagType(e) {
return { connect: 'success', disconnect: 'info', auth_fail: 'danger', quota_exceed: 'warning', rate_alert: 'warning' }[e] || 'info'
}
async function load() {
async function loadOnline() {
loading.value = true
try {
const [s, l, a] = await Promise.all([
const [s, a] = await Promise.all([
api.get('/sessions'),
api.get('/sessions/log'),
api.get('/traffic/alerts'),
])
sessions.value = s.data || []
logs.value = l.data || []
alerts.value = a.data || []
loadError.value = ''
sessionError.value = ''
} catch (e) {
loadError.value = e.response?.data?.error || t('sessions.loadDataFailed')
sessionError.value = e.response?.data?.error || t('sessions.loadDataFailed')
} finally { loading.value = false }
}
async function loadAudit() {
auditLoading.value = true
try {
const response = await api.get('/sessions/log')
logs.value = response.data || []
auditError.value = ''
} catch (e) {
auditError.value = e.response?.data?.error || t('sessions.loadDataFailed')
} finally { auditLoading.value = false }
}
function onTabChange(tab) {
activeTab.value = tab
if (tab === 'audit') loadAudit()
else loadOnline()
}
async function disconnect(row) {
try {
await ElMessageBox.confirm(t('sessions.kickConfirm', { username: row.display_name || displayUsername(row.username) }), t('common.confirm'), { type: 'warning' })
await api.post(`/sessions/${row.id}/disconnect`)
ElMessage.success(t('sessions.kicked'))
load()
loadOnline()
} catch (e) { if (e !== 'cancel') ElMessage.error(t('common.operationFailed')) }
}
let timer
onMounted(() => { load(); timer = setInterval(load, 15000) })
onMounted(() => {
if (activeTab.value === 'audit') loadAudit()
else loadOnline()
timer = setInterval(() => {
if (activeTab.value === 'online') loadOnline()
else loadAudit()
}, 15000)
})
onUnmounted(() => clearInterval(timer))
</script>