157 lines
6.0 KiB
Vue
157 lines
6.0 KiB
Vue
<template>
|
|
<div class="app">
|
|
<header class="header">
|
|
<h1>iOS 自动打包服务</h1>
|
|
<div class="header-right">
|
|
<nav v-if="isLoggedIn" class="nav">
|
|
<router-link to="/build">打包</router-link>
|
|
<router-link to="/history">历史</router-link>
|
|
<router-link to="/admin">管理</router-link>
|
|
<router-link v-if="isAdmin" to="/operations">操作日志</router-link>
|
|
</nav>
|
|
<div v-if="!isLoggedIn" class="user-info">
|
|
<button class="btn-login" @click="showLogin = true">登录</button>
|
|
</div>
|
|
<div v-else class="user-info">
|
|
<div class="user-avatar">{{ isAdmin ? 'A' : 'U' }}</div>
|
|
<span class="user-name">{{ isAdmin ? '管理员' : '用户' }}</span>
|
|
<button class="btn-logout" @click="logout">退出</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<template v-if="isLoggedIn">
|
|
<router-view />
|
|
</template>
|
|
<template v-else>
|
|
<div class="login-required">
|
|
<div class="login-card">
|
|
<h2>请先登录</h2>
|
|
<p>使用此服务需要登录账号</p>
|
|
<button class="login-btn" @click="showLogin = true">立即登录</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 登录弹窗 -->
|
|
<div v-if="showLogin" class="modal-overlay" @click.self="showLogin = false">
|
|
<div class="login-modal">
|
|
<h2>用户登录</h2>
|
|
<div class="form-group">
|
|
<label>用户名</label>
|
|
<input v-model="loginForm.username" type="text" placeholder="请输入用户名" @keyup.enter="login">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>密码</label>
|
|
<input v-model="loginForm.password" type="password" placeholder="请输入密码" @keyup.enter="login">
|
|
</div>
|
|
<button class="login-btn" @click="login">登录</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, provide } from 'vue'
|
|
|
|
const isLoggedIn = ref(false)
|
|
const isAdmin = ref(false)
|
|
const showLogin = ref(false)
|
|
const loginForm = ref({ username: '', password: '' })
|
|
|
|
// 提供给子组件使用
|
|
provide('showLogin', showLogin)
|
|
provide('isLoggedIn', isLoggedIn)
|
|
provide('isAdmin', isAdmin)
|
|
|
|
// 获取存储的 token
|
|
const getToken = () => localStorage.getItem('authToken') || ''
|
|
|
|
// 对外暴露 token 获取方法,供 API 请求使用
|
|
provide('getToken', getToken)
|
|
|
|
const login = async () => {
|
|
try {
|
|
const res = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(loginForm.value),
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
isLoggedIn.value = true
|
|
isAdmin.value = data.is_admin
|
|
showLogin.value = false
|
|
localStorage.setItem('authToken', data.token)
|
|
} else {
|
|
alert('用户名或密码错误')
|
|
}
|
|
} catch (e) {
|
|
alert('登录失败')
|
|
}
|
|
}
|
|
|
|
const logout = () => {
|
|
isLoggedIn.value = false
|
|
isAdmin.value = false
|
|
localStorage.removeItem('authToken')
|
|
}
|
|
|
|
// 检查登录状态(验证 token 有效性)
|
|
onMounted(async () => {
|
|
const token = localStorage.getItem('authToken')
|
|
if (!token) {
|
|
showLogin.value = true
|
|
return
|
|
}
|
|
try {
|
|
const res = await fetch('/api/auth/me', {
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
isLoggedIn.value = true
|
|
isAdmin.value = data.is_admin
|
|
} else {
|
|
localStorage.removeItem('authToken')
|
|
showLogin.value = true
|
|
}
|
|
} catch {
|
|
// 网络错误时保持当前状态
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; }
|
|
.header { background: #1a1a2e; color: white; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; }
|
|
.header h1 { font-size: 20px; }
|
|
.header-right { display: flex; align-items: center; gap: 20px; }
|
|
.nav { display: flex; gap: 20px; }
|
|
.nav a { color: #a0a0a0; text-decoration: none; padding: 8px 16px; border-radius: 6px; }
|
|
.nav a.router-link-active { background: #16213e; color: white; }
|
|
.nav a:hover { color: white; }
|
|
.user-info { display: flex; align-items: center; gap: 10px; }
|
|
.user-avatar { width: 32px; height: 32px; background: #1890ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; }
|
|
.user-name { font-size: 14px; }
|
|
.btn-login { background: #1890ff; color: white; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
|
.btn-logout { background: transparent; color: #a0a0a0; border: 1px solid #444; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
|
.btn-logout:hover { border-color: #ff4d4f; color: #ff4d4f; }
|
|
|
|
.login-required { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - 64px); }
|
|
.login-card { background: white; border-radius: 12px; padding: 48px; text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
|
|
.login-card h2 { font-size: 20px; color: #1a1a2e; margin-bottom: 8px; }
|
|
.login-card p { color: #999; margin-bottom: 24px; }
|
|
|
|
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
.login-modal { background: white; border-radius: 12px; padding: 32px; width: 400px; }
|
|
.login-modal h2 { font-size: 20px; margin-bottom: 24px; text-align: center; color: #1a1a2e; }
|
|
.form-group { margin-bottom: 16px; }
|
|
.form-group label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; font-weight: 500; }
|
|
.form-group input { width: 100%; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px; }
|
|
.form-group input:focus { outline: none; border-color: #1890ff; }
|
|
.login-btn { width: 100%; padding: 12px; background: #1890ff; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; margin-top: 8px; }
|
|
.login-btn:hover { background: #40a9ff; }
|
|
</style>
|