TaskPulse/frontend/src/App.vue
Steven cd41e59afc feat: add user authentication system
- User model (users table) with bcrypt password hashing
- JWT-based login API (POST /api/auth/login, GET /api/auth/me)
- Default admin account (admin/admin123) auto-created on startup
- Login page with centered dark-themed login card
- Route guards + Axios interceptors for token management
- Login page renders without sidebar (standalone layout)
- Gunicorn --preload to avoid worker DDL race condition
2026-06-14 23:22:53 +08:00

252 lines
8.0 KiB
Vue

<template>
<div id="app-container">
<!-- Login page: standalone without sidebar -->
<template v-if="isLoginPage">
<router-view />
</template>
<!-- App pages: with sidebar -->
<template v-else>
<!-- Sidebar -->
<aside class="sidebar">
<div class="logo">
<div class="logo-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
</div>
<span class="logo-text">TaskPulse</span>
</div>
<nav class="nav-menu">
<router-link to="/" class="nav-item" :class="{ active: currentRoute === '/' }">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
<span>仪表盘</span>
</router-link>
<router-link to="/tasks" class="nav-item" :class="{ active: currentRoute.startsWith('/tasks') }">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
<span>所有任务</span>
</router-link>
<router-link to="/agents" class="nav-item" :class="{ active: currentRoute.startsWith('/agents') }">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Agent 管理</span>
</router-link>
<router-link to="/alerts" class="nav-item" :class="{ active: currentRoute.startsWith('/alerts') }">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span>告警历史</span>
</router-link>
<router-link to="/settings" class="nav-item" :class="{ active: currentRoute.startsWith('/settings') }">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span>系统配置</span>
</router-link>
</nav>
<div class="sidebar-footer">
<div class="status-dot"></div>
<span>系统运行中</span>
</div>
</aside>
<!-- Main Content -->
<div class="main-area">
<header class="topbar">
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="topbar-right">
<template v-if="isLoggedIn">
<span class="user-name">{{ userName }}</span>
<button class="btn-logout" @click="handleLogout">退出</button>
</template>
<span v-else class="version-badge">v1.0</span>
</div>
</header>
<main class="content">
<router-view />
</main>
</div>
</template>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const currentRoute = computed(() => route.path)
const isLoginPage = computed(() => route.path === '/login')
const isLoggedIn = computed(() => !!localStorage.getItem('token'))
const userName = ref('')
// Load user info from localStorage
try {
const userData = JSON.parse(localStorage.getItem('user') || '{}')
userName.value = userData.display_name || userData.username || ''
} catch {}
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
router.push('/login')
}
const routeTitles = {
'/': '仪表盘',
'/tasks': '所有任务',
'/agents': 'Agent 管理',
'/alerts': '告警历史',
'/settings': '系统配置',
}
const pageTitle = computed(() => routeTitles[route.path] || 'TaskPulse')
</script>
<style scoped>
#app-container {
display: flex;
height: 100vh;
background: #0a0a0f;
color: #e0e0e0;
}
/* ── Sidebar ───────────────────────────────────── */
.sidebar {
width: 220px;
min-width: 220px;
background: linear-gradient(180deg, #0f0f1a 0%, #1a1a2e 100%);
border-right: 1px solid rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
padding: 0;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
padding: 24px 20px 20px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.logo-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.logo-text {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, #a8b4ff 0%, #c084fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-menu {
flex: 1;
padding: 12px 10px;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 10px;
color: rgba(255,255,255,0.55);
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
}
.nav-item:hover {
color: #fff;
background: rgba(255,255,255,0.06);
}
.nav-item.active {
color: #fff;
background: linear-gradient(135deg, rgba(102,126,234,0.2) 0%, rgba(118,75,162,0.2) 100%);
border: 1px solid rgba(102,126,234,0.15);
}
.sidebar-footer {
padding: 16px 20px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: rgba(255,255,255,0.35);
border-top: 1px solid rgba(255,255,255,0.06);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 6px rgba(34,197,94,0.5);
}
/* ── Main ──────────────────────────────────────── */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: #0a0a0f;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 28px;
background: rgba(15,15,26,0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #e0e0e0;
margin: 0;
}
.version-badge {
font-size: 11px;
padding: 3px 10px;
border-radius: 20px;
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.4);
border: 1px solid rgba(255,255,255,0.08);
}
.user-name {
font-size: 13px;
color: var(--text-secondary);
}
.btn-logout {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.04);
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
}
.btn-logout:hover {
color: #f87171;
border-color: rgba(239,68,68,0.2);
background: rgba(239,68,68,0.08);
}
.content {
flex: 1;
overflow-y: auto;
padding: 24px 28px;
}
</style>