feat: 前端认证
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<MainLayout />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MainLayout from './layouts/MainLayout.vue';
|
// App.vue 现在只需要渲染路由视图
|
||||||
|
// 路由器会决定是渲染 Login 组件还是 MainLayout 组件
|
||||||
</script>
|
</script>
|
||||||
|
17
web/src/api/auth.ts
Normal file
17
web/src/api/auth.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import apiClient from './index';
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
auth_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = async (authKey: string): Promise<LoginResponse> => {
|
||||||
|
const response = await apiClient.post<LoginResponse>('/auth/login', {
|
||||||
|
auth_key: authKey
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
@@ -1,4 +1,6 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: "/api",
|
baseURL: "/api",
|
||||||
@@ -7,8 +9,42 @@ const apiClient = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 可以添加请求和响应拦截器
|
// 请求拦截器:自动添加认证头
|
||||||
// apiClient.interceptors.request.use(...)
|
apiClient.interceptors.request.use(
|
||||||
// apiClient.interceptors.response.use(...)
|
(config) => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const authKey = authStore.getAuthKey();
|
||||||
|
|
||||||
|
if (authKey) {
|
||||||
|
config.headers.Authorization = `Bearer ${authKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器:处理401认证失败
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// 认证失败,清除登录状态并跳转到登录页
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
authStore.logout();
|
||||||
|
|
||||||
|
// 跳转到登录页(如果不在登录页的话)
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default apiClient;
|
export default apiClient;
|
||||||
|
@@ -1,54 +1,179 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container style="height: 100vh;">
|
<el-container style="height: 100vh;">
|
||||||
<el-aside width="200px">
|
<el-header class="header">
|
||||||
<el-menu
|
<div class="header-content">
|
||||||
:default-active="activeIndex"
|
<div class="header-title">
|
||||||
class="el-menu-vertical-demo"
|
<h3>GPT Load 管理面板</h3>
|
||||||
@select="handleSelect"
|
</div>
|
||||||
router
|
<div class="header-actions" v-if="authStore.isAuthenticated">
|
||||||
>
|
<el-dropdown @command="handleCommand">
|
||||||
<el-menu-item index="/dashboard">
|
<span class="el-dropdown-link">
|
||||||
<template #title>
|
<el-icon><User /></el-icon>
|
||||||
<span>Dashboard</span>
|
<span style="margin-left: 5px;">管理员</span>
|
||||||
</template>
|
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
</el-menu-item>
|
</span>
|
||||||
<el-menu-item index="/groups">
|
<template #dropdown>
|
||||||
<template #title>
|
<el-dropdown-menu>
|
||||||
<span>Groups</span>
|
<el-dropdown-item command="logout">
|
||||||
</template>
|
<el-icon><SwitchButton /></el-icon>
|
||||||
</el-menu-item>
|
退出登录
|
||||||
<el-menu-item index="/logs">
|
</el-dropdown-item>
|
||||||
<template #title>
|
</el-dropdown-menu>
|
||||||
<span>Logs</span>
|
</template>
|
||||||
</template>
|
</el-dropdown>
|
||||||
</el-menu-item>
|
</div>
|
||||||
<el-menu-item index="/settings">
|
</div>
|
||||||
<template #title>
|
</el-header>
|
||||||
<span>Settings</span>
|
|
||||||
</template>
|
<el-container>
|
||||||
</el-menu-item>
|
<el-aside width="200px">
|
||||||
</el-menu>
|
<el-menu
|
||||||
</el-aside>
|
:default-active="activeIndex"
|
||||||
<el-main>
|
class="el-menu-vertical-demo"
|
||||||
<router-view></router-view>
|
@select="handleSelect"
|
||||||
</el-main>
|
router
|
||||||
|
>
|
||||||
|
<el-menu-item index="/dashboard">
|
||||||
|
<el-icon><Odometer /></el-icon>
|
||||||
|
<template #title>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/groups">
|
||||||
|
<el-icon><Files /></el-icon>
|
||||||
|
<template #title>
|
||||||
|
<span>Groups</span>
|
||||||
|
</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/logs">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<template #title>
|
||||||
|
<span>Logs</span>
|
||||||
|
</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/settings">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<template #title>
|
||||||
|
<span>Settings</span>
|
||||||
|
</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-main>
|
||||||
|
<router-view></router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
ArrowDown,
|
||||||
|
SwitchButton,
|
||||||
|
Odometer,
|
||||||
|
Files,
|
||||||
|
Document,
|
||||||
|
Setting
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const activeIndex = ref(route.path)
|
const activeIndex = ref(route.path)
|
||||||
|
|
||||||
const handleSelect = (key: string, keyPath: string[]) => {
|
const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
console.log(key, keyPath)
|
console.log(key, keyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCommand = async (command: string) => {
|
||||||
|
if (command === 'logout') {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要退出登录吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
authStore.logout()
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
await router.push('/login')
|
||||||
|
} catch {
|
||||||
|
// 用户取消退出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-menu-vertical-demo {
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 60px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #606266;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-link:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-vertical-demo {
|
||||||
|
height: calc(100vh - 61px);
|
||||||
|
border-right: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item) {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item:hover) {
|
||||||
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -4,6 +4,7 @@ import App from './App.vue'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
import { useAuthStore } from './stores/authStore'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
@@ -11,4 +12,8 @@ app.use(createPinia())
|
|||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
// 确保认证状态在应用启动时初始化
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.initializeAuth()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@@ -1,33 +1,50 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import Dashboard from '../views/Dashboard.vue';
|
import { useAuthStore } from "../stores/authStore";
|
||||||
|
import Dashboard from "../views/Dashboard.vue";
|
||||||
|
import Login from "../views/Login.vue";
|
||||||
|
import MainLayout from "../layouts/MainLayout.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/login",
|
||||||
redirect: '/dashboard',
|
name: "Login",
|
||||||
|
component: Login,
|
||||||
|
meta: { requiresAuth: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: "/",
|
||||||
name: 'Dashboard',
|
component: MainLayout,
|
||||||
component: Dashboard,
|
meta: { requiresAuth: true },
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: '/groups',
|
path: "",
|
||||||
name: 'Groups',
|
redirect: "/dashboard",
|
||||||
// route level code-splitting
|
},
|
||||||
// this generates a separate chunk (About.[hash].js) for this route
|
{
|
||||||
// which is lazy-loaded when the route is visited.
|
path: "/dashboard",
|
||||||
component: () => import('../views/Groups.vue'),
|
name: "Dashboard",
|
||||||
},
|
component: Dashboard,
|
||||||
{
|
meta: { requiresAuth: true },
|
||||||
path: '/logs',
|
},
|
||||||
name: 'Logs',
|
{
|
||||||
component: () => import('../views/Logs.vue'),
|
path: "/groups",
|
||||||
},
|
name: "Groups",
|
||||||
{
|
component: () => import("../views/Groups.vue"),
|
||||||
path: '/settings',
|
meta: { requiresAuth: true },
|
||||||
name: 'Settings',
|
},
|
||||||
component: () => import('../views/Settings.vue'),
|
{
|
||||||
|
path: "/logs",
|
||||||
|
name: "Logs",
|
||||||
|
component: () => import("../views/Logs.vue"),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/settings",
|
||||||
|
name: "Settings",
|
||||||
|
component: () => import("../views/Settings.vue"),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -36,4 +53,27 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
// 路由守卫
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const isAuthenticated = authStore.isAuthenticated;
|
||||||
|
const requiresAuth = to.matched.some(
|
||||||
|
(record) => record.meta.requiresAuth !== false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requiresAuth && !isAuthenticated) {
|
||||||
|
// 需要认证但未登录,重定向到登录页
|
||||||
|
next({
|
||||||
|
name: "Login",
|
||||||
|
query: { redirect: to.fullPath },
|
||||||
|
});
|
||||||
|
} else if (to.name === "Login" && isAuthenticated) {
|
||||||
|
// 已登录用户访问登录页,重定向到仪表盘
|
||||||
|
next({ name: "Dashboard" });
|
||||||
|
} else {
|
||||||
|
// 正常访问
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
49
web/src/stores/authStore.ts
Normal file
49
web/src/stores/authStore.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const AUTH_KEY_STORAGE = 'gpt-load-auth-key';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
// State
|
||||||
|
const authKey = ref<string>('');
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const isAuthenticated = computed(() => !!authKey.value);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function login(key: string) {
|
||||||
|
authKey.value = key;
|
||||||
|
localStorage.setItem(AUTH_KEY_STORAGE, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
authKey.value = '';
|
||||||
|
localStorage.removeItem(AUTH_KEY_STORAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthKey(): string {
|
||||||
|
return authKey.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeAuth() {
|
||||||
|
const storedKey = localStorage.getItem(AUTH_KEY_STORAGE);
|
||||||
|
if (storedKey) {
|
||||||
|
authKey.value = storedKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在store初始化时自动恢复认证状态
|
||||||
|
initializeAuth();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
authKey,
|
||||||
|
// Computed
|
||||||
|
isAuthenticated,
|
||||||
|
// Actions
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
getAuthKey,
|
||||||
|
initializeAuth,
|
||||||
|
};
|
||||||
|
});
|
@@ -53,6 +53,11 @@ export interface Setting {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthUser {
|
||||||
|
key: string;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
}
|
||||||
export interface Setting {
|
export interface Setting {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
192
web/src/views/Login.vue
Normal file
192
web/src/views/Login.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<el-card class="login-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>GPT Load - 登录</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="loginFormRef"
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="loginRules"
|
||||||
|
class="login-form"
|
||||||
|
@submit.prevent="handleLogin"
|
||||||
|
>
|
||||||
|
<el-form-item prop="authKey">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.authKey"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入认证密钥"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Key /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
class="login-button"
|
||||||
|
>
|
||||||
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="errorMessage"
|
||||||
|
:title="errorMessage"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
class="error-alert"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, type FormInstance } from 'element-plus'
|
||||||
|
import { Key } from '@element-plus/icons-vue'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { login as loginAPI } from '../api/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const loginFormRef = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
authKey: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginRules = {
|
||||||
|
authKey: [
|
||||||
|
{ required: true, message: '请输入认证密钥', trigger: 'blur' },
|
||||||
|
{ min: 1, message: '认证密钥不能为空', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await loginFormRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
// 调用后端API进行认证
|
||||||
|
const response = await loginAPI(loginForm.authKey)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 认证成功,保存认证密钥
|
||||||
|
authStore.login(loginForm.authKey)
|
||||||
|
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 获取重定向路径,默认跳转到仪表盘
|
||||||
|
const redirect = router.currentRoute.value.query.redirect as string
|
||||||
|
await router.push(redirect || '/dashboard')
|
||||||
|
} else {
|
||||||
|
// 认证失败,显示错误信息
|
||||||
|
errorMessage.value = response.message || '认证失败,请检查认证密钥'
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
|
||||||
|
// 处理网络错误和API错误
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
errorMessage.value = '认证密钥错误,请重新输入'
|
||||||
|
} else if (error.response?.data?.message) {
|
||||||
|
errorMessage.value = error.response.data.message
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage.value = `登录失败: ${error.message}`
|
||||||
|
} else {
|
||||||
|
errorMessage.value = '登录失败,请检查网络连接'
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-alert {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
padding: 20px 20px 10px 20px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 30px 20px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 45px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -13,5 +13,14 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
outDir: 'dist'
|
outDir: 'dist'
|
||||||
},
|
},
|
||||||
base: './'
|
base: './',
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user