feat: login

This commit is contained in:
tbphp
2025-07-02 19:14:23 +08:00
parent c7d54db31a
commit 8e1de8d29f
10 changed files with 216 additions and 9 deletions

View File

@@ -1,7 +1,12 @@
<script setup lang="ts">
import Layout from "@/components/Layout.vue";
import { NDialogProvider, NMessageProvider } from "naive-ui";
</script>
<template>
<n-message-provider>
<n-dialog-provider>
<layout />
</n-dialog-provider>
</n-message-provider>
</template>

14
web/src/api/logs.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { Group, LogFilter, LogsResponse } from "@/types/models";
import http from "@/utils/http";
export const logApi = {
// 获取日志列表
getLogs: (params: LogFilter): Promise<LogsResponse> => {
return http.get("/logs", { params });
},
// 获取分组列表(用于筛选)
getGroups: (): Promise<Group[]> => {
return http.get("/groups");
},
};

View File

@@ -1,5 +1,5 @@
<template>
<n-layout>
<n-layout v-if="authService.isLoggedIn()">
<n-layout-header class="flex items-center">
<h1 class="layout-header-title">T.COM</h1>
<nav-bar />
@@ -9,11 +9,13 @@
<router-view />
</n-layout-content>
</n-layout>
<router-view v-else />
</template>
<script setup lang="ts">
import NavBar from "@/components/NavBar.vue";
import Logout from "@/components/Logout.vue";
import { authService } from "@/services/auth";
</script>
<style scoped>

View File

@@ -1,3 +1,15 @@
<template>
<n-button quaternary round>退出</n-button>
<n-button quaternary round @click="handleLogout">退出</n-button>
</template>
<script setup lang="ts">
import { authService } from "@/services/auth";
import { useRouter } from "vue-router";
const router = useRouter();
const handleLogout = () => {
authService.logout();
router.push("/login");
};
</script>

View File

@@ -1,7 +1,7 @@
import App from "@/App.vue";
import router from "@/router";
import naive from "naive-ui";
import { createApp } from "vue";
import App from "./App.vue";
import "./style.css";
import router from "./utils/router";
createApp(App).use(router).use(naive).mount("#app");

View File

@@ -1,3 +1,4 @@
import { authService } from "@/services/auth";
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
const routes: Array<RouteRecordRaw> = [
@@ -21,6 +22,11 @@ const routes: Array<RouteRecordRaw> = [
name: "settings",
component: () => import("@/views/Settings.vue"),
},
{
path: "/login",
name: "login",
component: () => import("@/views/Login.vue"),
},
];
const router = createRouter({
@@ -28,4 +34,17 @@ const router = createRouter({
routes,
});
router.beforeEach((to, _from, next) => {
const loggedIn = authService.isLoggedIn();
if (to.path !== "/login" && !loggedIn) {
return next({ path: "/login" });
}
if (to.path === "/login" && loggedIn) {
return next({ path: "/" });
}
next();
});
export default router;

33
web/src/services/auth.ts Normal file
View File

@@ -0,0 +1,33 @@
import http from "@/utils/http";
const AUTH_KEY = "authKey";
const login = async (authKey: string): Promise<boolean> => {
try {
await http.post("/auth/login", { auth_key: authKey });
localStorage.setItem(AUTH_KEY, authKey);
return true;
} catch (error) {
console.error("Login failed:", error);
return false;
}
};
const logout = (): void => {
localStorage.removeItem(AUTH_KEY);
};
const getAuthKey = (): string | null => {
return localStorage.getItem(AUTH_KEY);
};
const isLoggedIn = (): boolean => {
return !!localStorage.getItem(AUTH_KEY);
};
export const authService = {
login,
logout,
getAuthKey,
isLoggedIn,
};

63
web/src/types/models.ts Normal file
View File

@@ -0,0 +1,63 @@
// 数据模型定义
export interface APIKey {
id: number;
group_id: number;
key_value: string;
status: "active" | "inactive" | "error";
request_count: number;
failure_count: number;
last_used_at?: string;
created_at: string;
updated_at: string;
}
export interface Group {
id: number;
name: string;
description: string;
channel_type: "openai" | "gemini";
config: string;
api_keys?: APIKey[];
created_at: string;
updated_at: string;
}
export interface RequestLog {
id: string;
timestamp: string;
group_id: number;
key_id: number;
source_ip: string;
status_code: number;
request_path: string;
request_body_snippet: string;
}
export interface LogsResponse {
total: number;
page: number;
size: number;
data: RequestLog[];
}
export interface LogFilter {
page: number;
size: number;
group_id?: number;
start_time?: string;
end_time?: string;
status_code?: number;
source_ip?: string;
}
export interface DashboardStats {
total_requests: number;
success_requests: number;
success_rate: number;
group_stats: GroupRequestStat[];
}
export interface GroupRequestStat {
group_name: string;
request_count: number;
}

View File

@@ -1,4 +1,6 @@
import { authService } from "@/services/auth";
import axios from "axios";
import { useRouter } from "vue-router";
const http = axios.create({
baseURL: "/api",
@@ -8,9 +10,9 @@ const http = axios.create({
// 请求拦截器
http.interceptors.request.use(config => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
const authKey = authService.getAuthKey();
if (authKey) {
config.headers.Authorization = `Bearer ${authKey}`;
}
return config;
});
@@ -19,6 +21,10 @@ http.interceptors.request.use(config => {
http.interceptors.response.use(
response => response.data,
error => {
if (error.response && error.response.status === 401) {
authService.logout();
useRouter().push("/login");
}
console.error("API Error:", error);
return Promise.reject(error);
}

View File

@@ -1,3 +1,56 @@
<template>
<h1>Login</h1>
<div class="login-container">
<n-card class="login-card" title="Login">
<n-space vertical>
<n-input
v-model:value="authKey"
type="password"
placeholder="Auth Key"
@keyup.enter="handleLogin"
/>
<n-button type="primary" block @click="handleLogin" :loading="loading">Login</n-button>
</n-space>
</n-card>
</div>
</template>
<script setup lang="ts">
import { authService } from "@/services/auth";
import { NButton, NCard, NInput, NSpace, useMessage } from "naive-ui";
import { ref } from "vue";
import { useRouter } from "vue-router";
const authKey = ref("");
const loading = ref(false);
const router = useRouter();
const message = useMessage();
const handleLogin = async () => {
if (!authKey.value) {
message.error("Please enter Auth Key");
return;
}
loading.value = true;
const success = await authService.login(authKey.value);
loading.value = false;
if (success) {
router.push("/");
} else {
message.error("Login failed, please check your Auth Key");
}
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
.login-card {
width: 400px;
}
</style>