feat: login
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Layout from "@/components/Layout.vue";
|
import Layout from "@/components/Layout.vue";
|
||||||
|
import { NDialogProvider, NMessageProvider } from "naive-ui";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<layout />
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<layout />
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
</template>
|
</template>
|
||||||
|
14
web/src/api/logs.ts
Normal file
14
web/src/api/logs.ts
Normal 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");
|
||||||
|
},
|
||||||
|
};
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout>
|
<n-layout v-if="authService.isLoggedIn()">
|
||||||
<n-layout-header class="flex items-center">
|
<n-layout-header class="flex items-center">
|
||||||
<h1 class="layout-header-title">T.COM</h1>
|
<h1 class="layout-header-title">T.COM</h1>
|
||||||
<nav-bar />
|
<nav-bar />
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
<router-view />
|
<router-view />
|
||||||
</n-layout-content>
|
</n-layout-content>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
|
<router-view v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBar from "@/components/NavBar.vue";
|
import NavBar from "@/components/NavBar.vue";
|
||||||
import Logout from "@/components/Logout.vue";
|
import Logout from "@/components/Logout.vue";
|
||||||
|
import { authService } from "@/services/auth";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@@ -1,3 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-button quaternary round>退出</n-button>
|
<n-button quaternary round @click="handleLogout">退出</n-button>
|
||||||
</template>
|
</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>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
|
import App from "@/App.vue";
|
||||||
|
import router from "@/router";
|
||||||
import naive from "naive-ui";
|
import naive from "naive-ui";
|
||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import App from "./App.vue";
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import router from "./utils/router";
|
|
||||||
|
|
||||||
createApp(App).use(router).use(naive).mount("#app");
|
createApp(App).use(router).use(naive).mount("#app");
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { authService } from "@/services/auth";
|
||||||
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
|
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
@@ -21,6 +22,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "settings",
|
name: "settings",
|
||||||
component: () => import("@/views/Settings.vue"),
|
component: () => import("@/views/Settings.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: () => import("@/views/Login.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -28,4 +34,17 @@ const router = createRouter({
|
|||||||
routes,
|
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;
|
export default router;
|
33
web/src/services/auth.ts
Normal file
33
web/src/services/auth.ts
Normal 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
63
web/src/types/models.ts
Normal 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;
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
|
import { authService } from "@/services/auth";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: "/api",
|
baseURL: "/api",
|
||||||
@@ -8,9 +10,9 @@ const http = axios.create({
|
|||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
http.interceptors.request.use(config => {
|
http.interceptors.request.use(config => {
|
||||||
const token = localStorage.getItem("token");
|
const authKey = authService.getAuthKey();
|
||||||
if (token) {
|
if (authKey) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${authKey}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
@@ -19,6 +21,10 @@ http.interceptors.request.use(config => {
|
|||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
response => response.data,
|
response => response.data,
|
||||||
error => {
|
error => {
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
useRouter().push("/login");
|
||||||
|
}
|
||||||
console.error("API Error:", error);
|
console.error("API Error:", error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,56 @@
|
|||||||
<template>
|
<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>
|
</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>
|
||||||
|
Reference in New Issue
Block a user