dev
This commit is contained in:
259
domain-web/src/layouts/MainLayout.vue
Normal file
259
domain-web/src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="layout-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<strong>domainCheck</strong>
|
||||
<span>轻量管理后台</span>
|
||||
</div>
|
||||
<nav class="menu">
|
||||
<RouterLink v-for="item in menuItems" :key="item.path" :to="item.path" class="menu-item">
|
||||
<span>{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="main-area">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>当前保持桌面版不变,Web 管理后台进入可持续运维阶段。</p>
|
||||
</div>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<div class="status-pills">
|
||||
<span class="status-pill" :class="runtime.apiOnline ? 'online' : 'offline'">
|
||||
API {{ runtime.apiOnline ? "在线" : "离线" }}
|
||||
</span>
|
||||
<span class="status-pill" :class="runtime.workerOnline ? 'online' : 'offline'">
|
||||
Worker {{ runtime.workerOnline ? "在线" : "离线" }}
|
||||
</span>
|
||||
<span class="status-pill neutral">{{ runtime.workerMode }}</span>
|
||||
</div>
|
||||
<span class="user">{{ authStore.user.display_name }}</span>
|
||||
<el-button type="primary" plain @click="logout">退出</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { runtimeApi } from "@/api/modules";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const runtime = ref({
|
||||
apiOnline: false,
|
||||
workerOnline: false,
|
||||
workerMode: "windows-local"
|
||||
});
|
||||
let timer: number | null = null;
|
||||
|
||||
const menuItems = [
|
||||
{ path: "/dashboard", label: "概览" },
|
||||
{ path: "/runtime", label: "运行中心" },
|
||||
{ path: "/settings", label: "系统设置" },
|
||||
{ path: "/imports", label: "域名导入" },
|
||||
{ path: "/detect", label: "检测控制" },
|
||||
{ path: "/domains", label: "域名筛选" },
|
||||
{ path: "/exports", label: "导出中心" },
|
||||
{ path: "/logs", label: "日志诊断" }
|
||||
];
|
||||
|
||||
const title = computed(() => String(route.meta?.title || "概览"));
|
||||
|
||||
const loadRuntimeSummary = async () => {
|
||||
try {
|
||||
const response = await runtimeApi.status();
|
||||
runtime.value = {
|
||||
apiOnline: Boolean(response.data?.api?.pid),
|
||||
workerOnline: Boolean(response.data?.worker?.running),
|
||||
workerMode: response.data?.worker?.mode || "windows-local"
|
||||
};
|
||||
} catch {
|
||||
runtime.value = {
|
||||
apiOnline: false,
|
||||
workerOnline: false,
|
||||
workerMode: "unknown"
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
timer = window.setInterval(() => {
|
||||
loadRuntimeSummary();
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
authStore.clearToken();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadRuntimeSummary();
|
||||
startPolling();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: linear-gradient(180deg, #0f172a, #172554);
|
||||
color: #e5eefc;
|
||||
padding: 24px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.brand strong {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.brand span {
|
||||
color: rgba(229, 238, 252, 0.78);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
color: rgba(229, 238, 252, 0.86);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover,
|
||||
.menu-item.router-link-active {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 24px 28px 12px;
|
||||
}
|
||||
|
||||
.topbar h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.topbar p {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-pills {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-pill.online {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-pill.offline {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-pill.neutral {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.user {
|
||||
color: #334155;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 28px 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.layout-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user