feat(core): 初始化 Key-IP Sentinel 服务与部署骨架
- 搭建 FastAPI、Redis、PostgreSQL、Nginx 与 Docker Compose 基础结构 - 实现反向代理、首用绑定、拦截告警、归档任务和管理接口 - 提供 Vue3 管理后台初版,以及 uv/requirements 双依赖配置
This commit is contained in:
293
frontend/src/App.vue
Normal file
293
frontend/src/App.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { clearAuthToken } from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const clockLabel = ref('')
|
||||
let clockTimer
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', name: 'dashboard', icon: 'DataAnalysis' },
|
||||
{ label: 'Bindings', name: 'bindings', icon: 'Connection' },
|
||||
{ label: 'Logs', name: 'logs', icon: 'WarningFilled' },
|
||||
{ label: 'Settings', name: 'settings', icon: 'Setting' },
|
||||
]
|
||||
|
||||
const hideShell = computed(() => Boolean(route.meta.public))
|
||||
const currentSection = computed(() => route.meta.kicker || 'Operations')
|
||||
|
||||
function updateClock() {
|
||||
clockLabel.value = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(new Date())
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
clearAuthToken()
|
||||
await router.push({ name: 'login' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateClock()
|
||||
clockTimer = window.setInterval(updateClock, 60000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (clockTimer) {
|
||||
window.clearInterval(clockTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view v-if="hideShell" />
|
||||
|
||||
<div v-else class="shell">
|
||||
<div class="shell-glow shell-glow--mint" />
|
||||
<div class="shell-glow shell-glow--amber" />
|
||||
|
||||
<aside class="shell-sidebar panel">
|
||||
<div class="brand-block">
|
||||
<div class="brand-mark">S</div>
|
||||
<div>
|
||||
<p class="eyebrow">Key-IP Sentinel</p>
|
||||
<h1 class="brand-title">Control Plane</h1>
|
||||
<p class="brand-subtitle">First-use bind enforcement edge</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-list">
|
||||
<router-link
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:to="{ name: item.name }"
|
||||
class="nav-link"
|
||||
active-class="is-active"
|
||||
>
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-note">
|
||||
<p class="eyebrow">Operating mode</p>
|
||||
<h3>Zero-trust token perimeter</h3>
|
||||
<p class="muted">
|
||||
Every API key is pinned to the first observed client address or CIDR and inspected at the edge.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rail-grid">
|
||||
<div class="rail-card">
|
||||
<span class="rail-label">Surface</span>
|
||||
<strong>Admin UI</strong>
|
||||
<span class="rail-meta">JWT protected</span>
|
||||
</div>
|
||||
<div class="rail-card">
|
||||
<span class="rail-label">Proxy</span>
|
||||
<strong>Streaming</strong>
|
||||
<span class="rail-meta">SSE passthrough</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="shell-main">
|
||||
<header class="shell-header panel">
|
||||
<div class="header-copy">
|
||||
<p class="eyebrow">{{ currentSection }}</p>
|
||||
<h2 class="page-title">{{ route.meta.title || 'Sentinel' }}</h2>
|
||||
<p class="muted header-note">Edge policy, runtime settings, and operator visibility in one secure surface.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="header-chip-group">
|
||||
<div class="header-chip">
|
||||
<span class="header-chip-label">Mode</span>
|
||||
<strong>Secure Proxy</strong>
|
||||
</div>
|
||||
<div class="header-chip">
|
||||
<span class="header-chip-label">Updated</span>
|
||||
<strong>{{ clockLabel }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" plain @click="logout">Logout</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="shell-content">
|
||||
<router-view />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.shell-sidebar,
|
||||
.shell-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.shell-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, rgba(17, 231, 181, 0.95), rgba(21, 132, 214, 0.95));
|
||||
color: #071016;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand-title,
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.5rem, 2vw, 2.1rem);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
color: var(--sentinel-ink-soft);
|
||||
text-decoration: none;
|
||||
transition: transform 160ms ease, background 160ms ease, color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.is-active {
|
||||
color: var(--sentinel-ink);
|
||||
background: rgba(7, 176, 147, 0.14);
|
||||
box-shadow: inset 0 0 0 1px rgba(7, 176, 147, 0.18);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.sidebar-note {
|
||||
margin-top: auto;
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, rgba(8, 31, 45, 0.92), rgba(10, 26, 35, 0.8));
|
||||
color: #f3fffd;
|
||||
}
|
||||
|
||||
.sidebar-note h3 {
|
||||
margin: 10px 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.shell-main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.shell-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 22px 26px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.shell-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.shell-glow {
|
||||
position: fixed;
|
||||
inset: auto;
|
||||
z-index: 0;
|
||||
border-radius: 999px;
|
||||
filter: blur(90px);
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shell-glow--mint {
|
||||
top: 80px;
|
||||
right: 160px;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background: rgba(17, 231, 181, 0.22);
|
||||
}
|
||||
|
||||
.shell-glow--amber {
|
||||
bottom: 100px;
|
||||
left: 420px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background: rgba(255, 170, 76, 0.18);
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.shell-sidebar {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.shell-main {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.shell {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.shell-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user