{{ installData.command }}
+ {{ cmd }}${c}${cmd}${otherSection}`,
- `${name} ${t('dashboard.installCmd')}`, { dangerouslyUseHTMLString: true, confirmButtonText: t('dashboard.copyRecommended'),
- callback: (action) => { if (action === 'confirm') navigator.clipboard.writeText(cmd) } })
+ installData.value = { command: data.command || '', pm: data.pm || '', detected: !!data.detected, all_commands: data.all_commands || {} }
+ installDialogVisible.value = true
} catch {
ElMessage.error(t('dashboard.getInstallCmdFailed'))
}
}
+function copyText(text) { navigator.clipboard.writeText(text).then(() => ElMessage.success(t('common.copied'))) }
+
function fmtTime(t) { return t ? new Date(t).toLocaleString() : '-' }
const statCards = computed(() => [
@@ -153,14 +167,24 @@ const statCards = computed(() => [
{ label: t('dashboard.onlineUsers'), value: ov.value.online_sessions ?? 0, icon: Share, iconBg: 'linear-gradient(135deg,#e0935a,#c0762b)', to: '/sessions' },
{ label: t('dashboard.totalTraffic'), value: `${fmtBytes(ov.value.rx_bytes)} / ${fmtBytes(ov.value.tx_bytes)}`, icon: DataLine, iconBg: 'linear-gradient(135deg,#7b5ea7,#5a4080)', numStyle: { fontSize: '18px' } },
])
+const hostReady = computed(() => readiness.value.root && readiness.value.ipv4_forwarding && readiness.value.default_route_ready)
+const readinessHint = computed(() => {
+ const missing = []
+ if (!readiness.value.root) missing.push(t('dashboard.readinessRoot'))
+ if (!readiness.value.ipv4_forwarding) missing.push(t('dashboard.readinessForwarding'))
+ if (!readiness.value.default_route_ready) missing.push(t('dashboard.readinessRoute'))
+ return missing.join(' / ')
+})
async function load() {
// 轮询可见性守卫:标签页隐藏时跳过(P1 修复)
if (document.visibilityState !== 'visible') return
try {
- const [{ data: o }, { data: p }] = await Promise.all([api.get('/overview'), api.get('/protocols')])
+ const [{ data: o }, { data: p }, { data: r }] = await Promise.all([api.get('/overview'), api.get('/protocols'), api.get('/readiness')])
ov.value = o
protocols.value = p
+ readiness.value = r || {}
+ readinessLoaded.value = true
renderPie()
} catch { /* 忽略轮询错误 */ } finally { loadingProto.value = false }
}
@@ -235,4 +259,11 @@ onUnmounted(() => {
.stat-num { font-size: 26px; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
.dep-hint { font-size: 12px; color: var(--text-secondary); font-weight: normal; }
+.host-readiness { margin-top: var(--space-4); }
+.install-command { margin-top: var(--space-4); }
+.install-command-head, .other-command { display:flex; align-items:center; gap:8px; }
+.install-command-head { justify-content:space-between; }
+.command-block { margin:8px 0; padding:12px; background:var(--bg-inset); border:1px solid var(--border-base); white-space:pre-wrap; overflow:auto; }
+.other-command { margin-bottom:8px; }
+.other-command code { flex:1; min-width:0; overflow-wrap:anywhere; font-size:12px; }
diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue
index 511376d..1f2775c 100644
--- a/frontend/src/views/Layout.vue
+++ b/frontend/src/views/Layout.vue
@@ -14,9 +14,7 @@
{{ $t('vpnUsers.clickRowHint') }}
+ +{{ $t('vpnUsers.scanHint') }}
- -{{ $t('vpnUsers.mfaSetupIntro') }}
{{ mfaSecret }}{{ $t('vpnUsers.verifySetupHint') }}