|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<title>Auth Layout</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.layout-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
background: #1e88e5;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-user {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-avatar {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logout-btn {
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #1e88e5;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logout-btn:hover {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.main-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.menu {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
border-right: 1px solid #ddd;
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.menu-item {
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.menu-item:hover {
|
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.menu-item a {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.footer {
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
border-top: 1px solid #ddd;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
|
|
|
|
|
|
<body style="height: 100%;width: 100%;margin: 0;">
|
|
|
|
|
|
<div class="layout-container">
|
|
|
|
|
|
<header class="header">
|
|
|
|
|
|
<div class="header-title">
|
|
|
|
|
|
<a href="/">应用权限管理</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-user" v-if="user.name">
|
|
|
|
|
|
<img :src="user.icon" class="user-avatar" alt="用户头像">
|
|
|
|
|
|
<span class="user-name">{{ user.name }}</span>
|
|
|
|
|
|
<button @click="logout" class="logout-btn">
|
|
|
|
|
|
退出
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-user" v-else>
|
|
|
|
|
|
<span>未登录</span>
|
|
|
|
|
|
<a :href="$env.root ? $env.root + '/login' : '/login'" class="logout-btn">
|
|
|
|
|
|
登录
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="main-container">
|
|
|
|
|
|
<vslot v='user' class="menu" name='menu'>
|
|
|
|
|
|
<div class="menu-item">
|
|
|
|
|
|
<a href="/app">应用管理</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="menu-item">
|
|
|
|
|
|
<a href="/profile">个人中心</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="menu-item">
|
|
|
|
|
|
<a href="/settings">系统设置</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</vslot>
|
|
|
|
|
|
|
|
|
|
|
|
<vslot class="content">
|
|
|
|
|
|
</vslot>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="footer">
|
|
|
|
|
|
Copyright © 2025 veypi. All Rights Reserved..
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
user = {}
|
|
|
|
|
|
$env.user = user
|
|
|
|
|
|
|
|
|
|
|
|
class TokenService {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.tokenKey = 'access';
|
|
|
|
|
|
this.refreshTokenKey = 'refresh';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setToken(token) {
|
|
|
|
|
|
localStorage.setItem(this.tokenKey, token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getToken() {
|
|
|
|
|
|
return localStorage.getItem(this.tokenKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setRefreshToken(refreshToken) {
|
|
|
|
|
|
localStorage.setItem(this.refreshTokenKey, refreshToken);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getRefreshToken() {
|
|
|
|
|
|
return localStorage.getItem(this.refreshTokenKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearTokens() {
|
|
|
|
|
|
localStorage.removeItem(this.tokenKey);
|
|
|
|
|
|
localStorage.removeItem(this.refreshTokenKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hasToken() {
|
|
|
|
|
|
return !!this.getToken();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parseToken(token) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const base64Url = token.split('.')[1];
|
|
|
|
|
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
|
|
|
|
return JSON.parse(window.atob(base64));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Token解析失败:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async refreshToken() {
|
|
|
|
|
|
const refreshToken = this.getRefreshToken();
|
|
|
|
|
|
if (!refreshToken) {
|
|
|
|
|
|
logout();
|
|
|
|
|
|
this.clearTokens();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
let data = await api.Post('/api/token', {refresh: refreshToken})
|
|
|
|
|
|
this.setToken(data);
|
|
|
|
|
|
Object.assign(user, this.parseToken(data));
|
|
|
|
|
|
$env.user = user
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Token刷新失败:', error);
|
|
|
|
|
|
logout();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isTokenExpired() {
|
|
|
|
|
|
const token = this.getToken();
|
|
|
|
|
|
if (!token) return true;
|
|
|
|
|
|
|
|
|
|
|
|
const decoded = this.parseToken(token);
|
|
|
|
|
|
if (!decoded) return true;
|
|
|
|
|
|
|
|
|
|
|
|
const currentTime = Date.now() / 1000;
|
|
|
|
|
|
return decoded.exp < currentTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tokenService = new TokenService();
|
|
|
|
|
|
|
|
|
|
|
|
logout = () => {
|
|
|
|
|
|
let url = '/login';
|
|
|
|
|
|
if ($env.root) {
|
|
|
|
|
|
url = $env.root + url;
|
|
|
|
|
|
}
|
|
|
|
|
|
tokenService.clearTokens();
|
|
|
|
|
|
location.href = url;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await tokenService.refreshToken();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置API请求拦截器,自动添加token
|
|
|
|
|
|
api.wrapFetch((url, options) => {
|
|
|
|
|
|
const token = tokenService.getToken();
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
if (!options) {
|
|
|
|
|
|
options = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!options.headers) {
|
|
|
|
|
|
options.headers = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
options.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return fetch(url, options);
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
</html>
|