mirror of
https://github.com/Tencent/WeKnora.git
synced 2025-11-25 03:15:00 +08:00
feat: Add Login Page
This commit is contained in:
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "knowledage-base",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "knowledage-base",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"axios": "^1.8.4",
|
||||
|
||||
234
frontend/src/api/auth/index.ts
Normal file
234
frontend/src/api/auth/index.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { post, get, put } from '@/utils/request'
|
||||
|
||||
// 用户登录接口
|
||||
export interface LoginRequest {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
user?: {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
avatar?: string
|
||||
tenant_id: number
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
tenant?: {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
api_key: string
|
||||
status: string
|
||||
business: string
|
||||
storage_quota: number
|
||||
storage_used: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
token?: string
|
||||
refresh_token?: string
|
||||
}
|
||||
|
||||
// 用户注册接口
|
||||
export interface RegisterRequest {
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
user: {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
}
|
||||
tenant: {
|
||||
id: string
|
||||
name: string
|
||||
api_key: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息接口
|
||||
export interface UserInfo {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
avatar?: string
|
||||
tenant_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 租户信息接口
|
||||
export interface TenantInfo {
|
||||
id: string
|
||||
name: string
|
||||
api_key: string
|
||||
owner_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
knowledge_bases?: KnowledgeBaseInfo[]
|
||||
}
|
||||
|
||||
// 知识库信息接口
|
||||
export interface KnowledgeBaseInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
tenant_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
document_count?: number
|
||||
chunk_count?: number
|
||||
}
|
||||
|
||||
// 模型信息接口
|
||||
export interface ModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
source: string
|
||||
description?: string
|
||||
is_default?: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
export async function login(data: LoginRequest): Promise<LoginResponse> {
|
||||
try {
|
||||
const response = await post('/api/v1/auth/login', data)
|
||||
return response as unknown as LoginResponse
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '登录失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
export async function register(data: RegisterRequest): Promise<RegisterResponse> {
|
||||
try {
|
||||
const response = await post('/api/v1/auth/register', data)
|
||||
return response as unknown as RegisterResponse
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '注册失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<{ success: boolean; data?: UserInfo; message?: string }> {
|
||||
try {
|
||||
const response = await get('/api/v1/auth/me')
|
||||
return response as unknown as { success: boolean; data?: UserInfo; message?: string }
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '获取用户信息失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户信息
|
||||
*/
|
||||
export async function getCurrentTenant(): Promise<{ success: boolean; data?: TenantInfo; message?: string }> {
|
||||
try {
|
||||
const response = await get('/api/v1/auth/tenant')
|
||||
return response as unknown as { success: boolean; data?: TenantInfo; message?: string }
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '获取租户信息失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
export async function refreshToken(refreshToken: string): Promise<{ success: boolean; data?: { token: string; refreshToken: string }; message?: string }> {
|
||||
try {
|
||||
const response: any = await post('/api/v1/auth/refresh', { refreshToken })
|
||||
if (response && response.success) {
|
||||
if (response.access_token || response.refresh_token) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
token: response.access_token,
|
||||
refreshToken: response.refresh_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 其他情况直接返回原始消息
|
||||
return {
|
||||
success: false,
|
||||
message: response?.message || '刷新Token失败'
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '刷新Token失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
export async function logout(): Promise<{ success: boolean; message?: string }> {
|
||||
try {
|
||||
await post('/api/v1/auth/logout', {})
|
||||
return {
|
||||
success: true
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '登出失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token有效性
|
||||
*/
|
||||
export async function validateToken(): Promise<{ success: boolean; valid?: boolean; message?: string }> {
|
||||
try {
|
||||
const response = await get('/api/v1/auth/validate')
|
||||
return response as unknown as { success: boolean; valid?: boolean; message?: string }
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
valid: false,
|
||||
message: error.message || 'Token验证失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,54 +1,30 @@
|
||||
import { get, post, put, del, postChat } from "../../utils/request";
|
||||
import { loadTestData } from "../test-data";
|
||||
|
||||
// 从localStorage获取设置
|
||||
function getSettings() {
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
if (settingsStr) {
|
||||
try {
|
||||
const settings = JSON.parse(settingsStr);
|
||||
if (settings.apiKey && settings.endpoint) {
|
||||
return settings;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("解析设置失败:", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据是否有设置决定是否需要加载测试数据
|
||||
async function ensureConfigured() {
|
||||
const settings = getSettings();
|
||||
// 如果没有设置APIKey和Endpoint,则加载测试数据
|
||||
if (!settings) {
|
||||
await loadTestData();
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSessions(data = {}) {
|
||||
await ensureConfigured();
|
||||
await loadTestData();
|
||||
return post("/api/v1/sessions", data);
|
||||
}
|
||||
|
||||
export async function getSessionsList(page: number, page_size: number) {
|
||||
await ensureConfigured();
|
||||
await loadTestData();
|
||||
return get(`/api/v1/sessions?page=${page}&page_size=${page_size}`);
|
||||
}
|
||||
|
||||
export async function generateSessionsTitle(session_id: string, data: any) {
|
||||
await ensureConfigured();
|
||||
await loadTestData();
|
||||
return post(`/api/v1/sessions/${session_id}/generate_title`, data);
|
||||
}
|
||||
|
||||
export async function knowledgeChat(data: { session_id: string; query: string; }) {
|
||||
await ensureConfigured();
|
||||
await loadTestData();
|
||||
return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query });
|
||||
}
|
||||
|
||||
export async function getMessageList(data: { session_id: string; limit: number, created_at: string }) {
|
||||
await ensureConfigured();
|
||||
|
||||
await loadTestData();
|
||||
if (data.created_at) {
|
||||
return get(`/api/v1/messages/${data.session_id}/load?before_time=${encodeURIComponent(data.created_at)}&limit=${data.limit}`);
|
||||
} else {
|
||||
@@ -57,6 +33,6 @@ export async function getMessageList(data: { session_id: string; limit: number,
|
||||
}
|
||||
|
||||
export async function delSession(session_id: string) {
|
||||
await ensureConfigured();
|
||||
await loadTestData();
|
||||
return del(`/api/v1/sessions/${session_id}`);
|
||||
}
|
||||
@@ -2,21 +2,9 @@ import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { ref, type Ref, onUnmounted, nextTick } from 'vue'
|
||||
import { generateRandomString } from '@/utils/index';
|
||||
import { getTestData } from '@/utils/request';
|
||||
import { loadTestData } from '@/api/test-data';
|
||||
import { loadTestData } from "../test-data";
|
||||
|
||||
|
||||
// 从localStorage获取设置
|
||||
function getSettings() {
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
if (settingsStr) {
|
||||
try {
|
||||
const settings = JSON.parse(settingsStr);
|
||||
return settings;
|
||||
} catch (e) {
|
||||
console.error("解析设置失败:", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface StreamOptions {
|
||||
// 请求方法 (默认POST)
|
||||
@@ -49,27 +37,16 @@ export function useStream() {
|
||||
isStreaming.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
// 获取设置信息
|
||||
const settings = getSettings();
|
||||
let apiUrl = '';
|
||||
let apiKey = '';
|
||||
|
||||
// 如果有设置信息,优先使用设置信息
|
||||
if (settings && settings.endpoint && settings.apiKey) {
|
||||
apiUrl = settings.endpoint;
|
||||
apiKey = settings.apiKey;
|
||||
} else {
|
||||
// 否则加载测试数据
|
||||
await loadTestData();
|
||||
const testData = getTestData();
|
||||
if (!testData) {
|
||||
error.value = "测试数据未初始化,无法进行聊天";
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
|
||||
apiKey = testData.tenant.api_key;
|
||||
// 使用默认配置
|
||||
await loadTestData();
|
||||
const testData = getTestData();
|
||||
if (!testData) {
|
||||
error.value = "测试数据未初始化,无法进行聊天";
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
const apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
|
||||
const apiKey = testData.tenant.api_key;
|
||||
|
||||
try {
|
||||
let url =
|
||||
|
||||
@@ -70,6 +70,11 @@ export function checkInitializationStatus(): Promise<{ initialized: boolean }> {
|
||||
resolve(response.data || { initialized: false });
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 如果是401,交给全局拦截器去处理(重定向登录),这里不要把它当成未初始化
|
||||
if (error && error.status === 401) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
console.warn('检查初始化状态失败,假设需要初始化:', error);
|
||||
resolve({ initialized: false });
|
||||
});
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
import { get, post, put, del, postUpload, getDown, getTestData } from "../../utils/request";
|
||||
import { loadTestData } from "../test-data";
|
||||
|
||||
// 获取知识库ID(优先从设置中获取)
|
||||
async function getKnowledgeBaseID() {
|
||||
// 从localStorage获取设置中的知识库ID
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
let knowledgeBaseId = "";
|
||||
|
||||
if (settingsStr) {
|
||||
try {
|
||||
const settings = JSON.parse(settingsStr);
|
||||
if (settings.knowledgeBaseId) {
|
||||
return settings.knowledgeBaseId;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("解析设置失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDefaultKnowledgeBaseId(): Promise<string> {
|
||||
// 如果设置中没有知识库ID,则使用测试数据
|
||||
await loadTestData();
|
||||
|
||||
const testData = getTestData();
|
||||
if (!testData || testData.knowledge_bases.length === 0) {
|
||||
console.error("测试数据未初始化或不包含知识库");
|
||||
throw new Error("测试数据未初始化或不包含知识库");
|
||||
throw new Error('没有可用的知识库');
|
||||
}
|
||||
|
||||
return testData.knowledge_bases[0].id;
|
||||
}
|
||||
|
||||
export async function uploadKnowledgeBase(data = {}) {
|
||||
const kbId = await getKnowledgeBaseID();
|
||||
const kbId = await getDefaultKnowledgeBaseId();
|
||||
return postUpload(`/api/v1/knowledge-bases/${kbId}/knowledge/file`, data);
|
||||
}
|
||||
|
||||
export async function getKnowledgeBase({page, page_size}) {
|
||||
const kbId = await getKnowledgeBaseID();
|
||||
export async function getKnowledgeBase({page, page_size}: {page: number, page_size: number}) {
|
||||
const kbId = await getDefaultKnowledgeBaseId();
|
||||
return get(
|
||||
`/api/v1/knowledge-bases/${kbId}/knowledge?page=${page}&page_size=${page_size}`
|
||||
);
|
||||
@@ -57,6 +39,6 @@ export function batchQueryKnowledge(ids: any) {
|
||||
return get(`/api/v1/knowledge/batch?${ids}`);
|
||||
}
|
||||
|
||||
export function getKnowledgeDetailsCon(id: any, page) {
|
||||
export function getKnowledgeDetailsCon(id: any, page: number) {
|
||||
return get(`/api/v1/chunks/${id}?page=${page}&page_size=25`);
|
||||
}
|
||||
@@ -53,3 +53,12 @@ export async function loadTestData(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置测试数据加载状态,在重新登录或需要强制刷新时调用
|
||||
*/
|
||||
export function resetTestDataLoaded() {
|
||||
isTestDataLoaded = false;
|
||||
// 清空已缓存的测试数据,确保下次调用会重新获取
|
||||
setTestData(null);
|
||||
}
|
||||
|
||||
6
frontend/src/assets/img/logout.svg
Normal file
6
frontend/src/assets/img/logout.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4" stroke="#000" stroke-opacity="0.6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17 16l4-4-4-4" stroke="#000" stroke-opacity="0.6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 12H10" stroke="#000" stroke-opacity="0.6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 509 B |
@@ -9,7 +9,7 @@
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
|
||||
<div class="menu_item-box">
|
||||
<div class="menu_icon">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : prefixIcon)" alt="">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : prefixIcon)" alt="">
|
||||
</div>
|
||||
<span class="menu_title">{{ item.title }}</span>
|
||||
</div>
|
||||
@@ -58,11 +58,13 @@ import { onMounted, watch, computed, ref, reactive } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getSessionsList, delSession } from "@/api/chat/index";
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
let { requestMethod } = useKnowledgeBase()
|
||||
let uploadInput = ref();
|
||||
const usemenuStore = useMenuStore();
|
||||
const authStore = useAuthStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const currentpath = ref('');
|
||||
@@ -164,12 +166,14 @@ let fileAddIcon = ref('file-add-green.svg');
|
||||
let knowledgeIcon = ref('zhishiku-green.svg');
|
||||
let prefixIcon = ref('prefixIcon.svg');
|
||||
let settingIcon = ref('setting.svg');
|
||||
let logoutIcon = ref('logout.svg');
|
||||
let pathPrefix = ref(route.name)
|
||||
const getIcon = (path) => {
|
||||
fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg';
|
||||
knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg';
|
||||
prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg';
|
||||
settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg';
|
||||
logoutIcon.value = 'logout.svg';
|
||||
}
|
||||
getIcon(route.name)
|
||||
const gotopage = (path) => {
|
||||
@@ -177,6 +181,13 @@ const gotopage = (path) => {
|
||||
// 如果是系统设置,跳转到初始化配置页面
|
||||
if (path === 'settings') {
|
||||
router.push('/initialization');
|
||||
return;
|
||||
}
|
||||
// 处理退出登录
|
||||
if (path === 'logout') {
|
||||
authStore.logout();
|
||||
router.push('/login');
|
||||
return;
|
||||
} else {
|
||||
router.push(`/platform/${path}`);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { checkInitializationStatus } from '@/api/initialization'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { validateToken } from '@/api/auth'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/platform",
|
||||
redirect: "/platform/knowledgeBase",
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: () => import("../views/auth/Login.vue"),
|
||||
meta: { requiresAuth: false, requiresInit: false }
|
||||
},
|
||||
{
|
||||
path: "/initialization",
|
||||
@@ -18,71 +26,110 @@ const router = createRouter({
|
||||
path: "/knowledgeBase",
|
||||
name: "home",
|
||||
component: () => import("../views/knowledge/KnowledgeBase.vue"),
|
||||
meta: { requiresInit: true }
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/platform",
|
||||
name: "Platform",
|
||||
redirect: "/platform/knowledgeBase",
|
||||
component: () => import("../views/platform/index.vue"),
|
||||
meta: { requiresInit: true },
|
||||
meta: { requiresInit: true, requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: "knowledgeBase",
|
||||
name: "knowledgeBase",
|
||||
component: () => import("../views/knowledge/KnowledgeBase.vue"),
|
||||
meta: { requiresInit: true }
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "creatChat",
|
||||
name: "creatChat",
|
||||
component: () => import("../views/creatChat/creatChat.vue"),
|
||||
meta: { requiresInit: true }
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "chat/:chatid",
|
||||
name: "chat",
|
||||
component: () => import("../views/chat/index.vue"),
|
||||
meta: { requiresInit: true }
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
name: "settings",
|
||||
component: () => import("../views/settings/Settings.vue"),
|
||||
path: "settings",
|
||||
name: "settings",
|
||||
component: () => import("../views/settings/Settings.vue"),
|
||||
meta: { requiresInit: true }
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 路由守卫:检查系统初始化状态
|
||||
// 路由守卫:检查认证状态和系统初始化状态
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 如果访问的是初始化页面,直接放行
|
||||
if (to.meta.requiresInit === false) {
|
||||
next();
|
||||
return;
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 如果访问的是登录页面或初始化页面,直接放行
|
||||
if (to.meta.requiresAuth === false || to.meta.requiresInit === false) {
|
||||
// 如果已登录用户访问登录页面,重定向到知识库列表页面
|
||||
if (to.path === '/login' && authStore.isLoggedIn) {
|
||||
next('/platform/knowledgeBase')
|
||||
return
|
||||
}
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
1
|
||||
|
||||
try {
|
||||
// 检查系统是否已初始化
|
||||
const { initialized } = await checkInitializationStatus();
|
||||
|
||||
if (initialized) {
|
||||
// 系统已初始化,记录到本地存储并正常跳转
|
||||
localStorage.setItem('system_initialized', 'true');
|
||||
next();
|
||||
} else {
|
||||
// 系统未初始化,跳转到初始化页面
|
||||
console.log('系统未初始化,跳转到初始化页面');
|
||||
next('/initialization');
|
||||
// 检查用户认证状态
|
||||
if (to.meta.requiresAuth !== false) {
|
||||
if (!authStore.isLoggedIn) {
|
||||
// 未登录,跳转到登录页面
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查初始化状态失败:', error);
|
||||
// 如果检查失败,默认认为需要初始化
|
||||
next('/initialization');
|
||||
|
||||
// 验证Token有效性
|
||||
// try {
|
||||
// const { valid } = await validateToken()
|
||||
// if (!valid) {
|
||||
// // Token无效,清空认证信息并跳转到登录页面
|
||||
// authStore.logout()
|
||||
// next('/login')
|
||||
// return
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Token验证失败:', error)
|
||||
// authStore.logout()
|
||||
// next('/login')
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
// 检查系统初始化状态
|
||||
if (to.meta.requiresInit !== false) {
|
||||
try {
|
||||
const { initialized } = await checkInitializationStatus()
|
||||
|
||||
if (initialized) {
|
||||
// 系统已初始化,记录到本地存储并正常跳转
|
||||
localStorage.setItem('system_initialized', 'true')
|
||||
next()
|
||||
} else {
|
||||
// 系统未初始化,跳转到初始化页面
|
||||
next('/initialization')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查初始化状态失败:', error)
|
||||
// 如果是401,跳转登录,不再误导去初始化
|
||||
const status = (error as any)?.status
|
||||
if (status === 401) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
// 其他错误默认认为需要初始化
|
||||
next('/initialization')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
174
frontend/src/stores/auth.ts
Normal file
174
frontend/src/stores/auth.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { resetTestDataLoaded } from '@/api/test-data'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { UserInfo, TenantInfo, KnowledgeBaseInfo } from '@/api/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref<UserInfo | null>(null)
|
||||
const tenant = ref<TenantInfo | null>(null)
|
||||
const token = ref<string>('')
|
||||
const refreshToken = ref<string>('')
|
||||
const knowledgeBases = ref<KnowledgeBaseInfo[]>([])
|
||||
const currentKnowledgeBase = ref<KnowledgeBaseInfo | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => {
|
||||
return !!token.value && !!user.value
|
||||
})
|
||||
|
||||
const hasValidTenant = computed(() => {
|
||||
return !!tenant.value && !!tenant.value.api_key
|
||||
})
|
||||
|
||||
const currentTenantId = computed(() => {
|
||||
return tenant.value?.id || ''
|
||||
})
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
return user.value?.id || ''
|
||||
})
|
||||
|
||||
// 操作方法
|
||||
const setUser = (userData: UserInfo) => {
|
||||
user.value = userData
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('weknora_user', JSON.stringify(userData))
|
||||
}
|
||||
|
||||
const setTenant = (tenantData: TenantInfo) => {
|
||||
tenant.value = tenantData
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('weknora_tenant', JSON.stringify(tenantData))
|
||||
}
|
||||
|
||||
const setToken = (tokenValue: string) => {
|
||||
token.value = tokenValue
|
||||
localStorage.setItem('weknora_token', tokenValue)
|
||||
}
|
||||
|
||||
const setRefreshToken = (refreshTokenValue: string) => {
|
||||
refreshToken.value = refreshTokenValue
|
||||
localStorage.setItem('weknora_refresh_token', refreshTokenValue)
|
||||
}
|
||||
|
||||
const setKnowledgeBases = (kbList: KnowledgeBaseInfo[]) => {
|
||||
// 确保输入是数组
|
||||
knowledgeBases.value = Array.isArray(kbList) ? kbList : []
|
||||
localStorage.setItem('weknora_knowledge_bases', JSON.stringify(knowledgeBases.value))
|
||||
}
|
||||
|
||||
const setCurrentKnowledgeBase = (kb: KnowledgeBaseInfo | null) => {
|
||||
currentKnowledgeBase.value = kb
|
||||
if (kb) {
|
||||
localStorage.setItem('weknora_current_kb', JSON.stringify(kb))
|
||||
} else {
|
||||
localStorage.removeItem('weknora_current_kb')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const logout = () => {
|
||||
// 清空状态
|
||||
user.value = null
|
||||
tenant.value = null
|
||||
token.value = ''
|
||||
refreshToken.value = ''
|
||||
knowledgeBases.value = []
|
||||
currentKnowledgeBase.value = null
|
||||
|
||||
// 清空localStorage
|
||||
localStorage.removeItem('weknora_user')
|
||||
localStorage.removeItem('weknora_tenant')
|
||||
localStorage.removeItem('weknora_token')
|
||||
localStorage.removeItem('weknora_refresh_token')
|
||||
localStorage.removeItem('weknora_knowledge_bases')
|
||||
localStorage.removeItem('weknora_current_kb')
|
||||
|
||||
// 重置测试数据加载标志,确保重新登录后会重新获取KB列表
|
||||
try {
|
||||
resetTestDataLoaded()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const initFromStorage = () => {
|
||||
// 从localStorage恢复状态
|
||||
const storedUser = localStorage.getItem('weknora_user')
|
||||
const storedTenant = localStorage.getItem('weknora_tenant')
|
||||
const storedToken = localStorage.getItem('weknora_token')
|
||||
const storedRefreshToken = localStorage.getItem('weknora_refresh_token')
|
||||
const storedKnowledgeBases = localStorage.getItem('weknora_knowledge_bases')
|
||||
const storedCurrentKb = localStorage.getItem('weknora_current_kb')
|
||||
|
||||
if (storedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(storedUser)
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (storedTenant) {
|
||||
try {
|
||||
tenant.value = JSON.parse(storedTenant)
|
||||
} catch (e) {
|
||||
console.error('解析租户信息失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (storedToken) {
|
||||
token.value = storedToken
|
||||
}
|
||||
|
||||
if (storedRefreshToken) {
|
||||
refreshToken.value = storedRefreshToken
|
||||
}
|
||||
|
||||
if (storedKnowledgeBases) {
|
||||
try {
|
||||
const parsed = JSON.parse(storedKnowledgeBases)
|
||||
knowledgeBases.value = Array.isArray(parsed) ? parsed : []
|
||||
} catch (e) {
|
||||
console.error('解析知识库列表失败:', e)
|
||||
knowledgeBases.value = []
|
||||
}
|
||||
}
|
||||
|
||||
if (storedCurrentKb) {
|
||||
try {
|
||||
currentKnowledgeBase.value = JSON.parse(storedCurrentKb)
|
||||
} catch (e) {
|
||||
console.error('解析当前知识库失败:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时从localStorage恢复状态
|
||||
initFromStorage()
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
tenant,
|
||||
token,
|
||||
refreshToken,
|
||||
knowledgeBases,
|
||||
currentKnowledgeBase,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
hasValidTenant,
|
||||
currentTenantId,
|
||||
currentUserId,
|
||||
|
||||
// 方法
|
||||
setUser,
|
||||
setTenant,
|
||||
setToken,
|
||||
setRefreshToken,
|
||||
setKnowledgeBases,
|
||||
setCurrentKnowledgeBase,
|
||||
logout,
|
||||
initFromStorage
|
||||
}
|
||||
})
|
||||
@@ -13,7 +13,8 @@ export const useMenuStore = defineStore('menuStore', {
|
||||
childrenPath: 'chat',
|
||||
children: reactive<object[]>([]),
|
||||
},
|
||||
{ title: '系统设置', icon: 'setting', path: 'settings' }
|
||||
{ title: '系统设置', icon: 'setting', path: 'settings' },
|
||||
{ title: '退出登录', icon: 'logout', path: 'logout' }
|
||||
]),
|
||||
isFirstSession: false,
|
||||
firstQuery: ''
|
||||
|
||||
@@ -2,26 +2,8 @@
|
||||
import axios from "axios";
|
||||
import { generateRandomString } from "./index";
|
||||
|
||||
// 从localStorage获取设置
|
||||
function getSettings() {
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
if (settingsStr) {
|
||||
try {
|
||||
return JSON.parse(settingsStr);
|
||||
} catch (e) {
|
||||
console.error("解析设置失败:", e);
|
||||
}
|
||||
}
|
||||
return {
|
||||
endpoint: import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080",
|
||||
apiKey: "",
|
||||
knowledgeBaseId: "",
|
||||
};
|
||||
}
|
||||
|
||||
// API基础URL,优先使用设置中的endpoint
|
||||
const settings = getSettings();
|
||||
const BASE_URL = settings.endpoint;
|
||||
// API基础URL
|
||||
const BASE_URL = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
|
||||
|
||||
// 测试数据
|
||||
let testData: {
|
||||
@@ -50,13 +32,6 @@ const instance = axios.create({
|
||||
// 设置测试数据
|
||||
export function setTestData(data: typeof testData) {
|
||||
testData = data;
|
||||
if (data) {
|
||||
// 优先使用设置中的ApiKey,如果没有则使用测试数据中的
|
||||
const apiKey = settings.apiKey || (data?.tenant?.api_key || "");
|
||||
if (apiKey) {
|
||||
instance.defaults.headers["X-API-Key"] = apiKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取测试数据
|
||||
@@ -66,25 +41,38 @@ export function getTestData() {
|
||||
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 每次请求前检查是否有更新的设置
|
||||
const currentSettings = getSettings();
|
||||
|
||||
// 更新BaseURL (如果有变化)
|
||||
if (currentSettings.endpoint && config.baseURL !== currentSettings.endpoint) {
|
||||
config.baseURL = currentSettings.endpoint;
|
||||
}
|
||||
|
||||
// 更新API Key (如果有)
|
||||
if (currentSettings.apiKey) {
|
||||
config.headers["X-API-Key"] = currentSettings.apiKey;
|
||||
// 添加JWT token认证
|
||||
const token = localStorage.getItem('weknora_token');
|
||||
if (token) {
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
config.headers["X-Request-ID"] = `${generateRandomString(12)}`;
|
||||
return config;
|
||||
},
|
||||
(error) => {}
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Token刷新标志,防止多个请求同时刷新token
|
||||
let isRefreshing = false;
|
||||
let failedQueue: Array<{ resolve: Function; reject: Function }> = [];
|
||||
let hasRedirectedOn401 = false;
|
||||
|
||||
// 处理队列中的请求
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(token);
|
||||
}
|
||||
});
|
||||
|
||||
failedQueue = [];
|
||||
};
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
// 根据业务状态码处理逻辑
|
||||
@@ -95,12 +83,98 @@ instance.interceptors.response.use(
|
||||
return Promise.reject(data);
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
async (error: any) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (!error.response) {
|
||||
return Promise.reject({ message: "网络错误,请检查您的网络连接" });
|
||||
}
|
||||
const { data } = error.response;
|
||||
return Promise.reject(data);
|
||||
|
||||
// 如果是登录接口的401,直接返回错误以便页面展示toast,不做跳转
|
||||
if (error.response.status === 401 && originalRequest?.url?.includes('/auth/login')) {
|
||||
const { status, data } = error.response;
|
||||
return Promise.reject({ status, message: (typeof data === 'object' ? data?.message : data) || '用户名或密码错误' });
|
||||
}
|
||||
|
||||
// 如果是401错误且不是刷新token的请求,尝试刷新token
|
||||
if (error.response.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/refresh')) {
|
||||
if (isRefreshing) {
|
||||
// 如果正在刷新token,将请求加入队列
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
}).then(token => {
|
||||
originalRequest.headers['Authorization'] = 'Bearer ' + token;
|
||||
return instance(originalRequest);
|
||||
}).catch(err => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
isRefreshing = true;
|
||||
|
||||
const refreshToken = localStorage.getItem('weknora_refresh_token');
|
||||
|
||||
if (refreshToken) {
|
||||
try {
|
||||
// 动态导入refresh token API
|
||||
const { refreshToken: refreshTokenAPI } = await import('../api/auth/index');
|
||||
const response = await refreshTokenAPI(refreshToken);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const { token, refreshToken: newRefreshToken } = response.data;
|
||||
|
||||
// 更新localStorage中的token
|
||||
localStorage.setItem('weknora_token', token);
|
||||
localStorage.setItem('weknora_refresh_token', newRefreshToken);
|
||||
|
||||
// 更新请求头
|
||||
originalRequest.headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
// 处理队列中的请求
|
||||
processQueue(null, token);
|
||||
|
||||
return instance(originalRequest);
|
||||
} else {
|
||||
throw new Error(response.message || 'Token刷新失败');
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除所有token并跳转到登录页
|
||||
localStorage.removeItem('weknora_token');
|
||||
localStorage.removeItem('weknora_refresh_token');
|
||||
localStorage.removeItem('weknora_user');
|
||||
localStorage.removeItem('weknora_tenant');
|
||||
|
||||
processQueue(refreshError, null);
|
||||
|
||||
// 跳转到登录页
|
||||
if (!hasRedirectedOn401 && typeof window !== 'undefined') {
|
||||
hasRedirectedOn401 = true;
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
} else {
|
||||
// 没有refresh token,直接跳转到登录页
|
||||
localStorage.removeItem('weknora_token');
|
||||
localStorage.removeItem('weknora_user');
|
||||
localStorage.removeItem('weknora_tenant');
|
||||
|
||||
if (!hasRedirectedOn401 && typeof window !== 'undefined') {
|
||||
hasRedirectedOn401 = true;
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return Promise.reject({ message: '请重新登录' });
|
||||
}
|
||||
}
|
||||
|
||||
const { status, data } = error.response;
|
||||
// 将HTTP状态码一并抛出,方便上层判断401等场景
|
||||
return Promise.reject({ status, ...(typeof data === 'object' ? data : { message: data }) });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
559
frontend/src/views/auth/Login.vue
Normal file
559
frontend/src/views/auth/Login.vue
Normal file
@@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<!-- 登录表单 -->
|
||||
<div class="login-card" v-if="!isRegisterMode">
|
||||
<!-- 系统Logo和标题 -->
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<img src="@/assets/img/weknora.png" alt="WeKnora" class="logo-img" />
|
||||
</div>
|
||||
<p class="login-subtitle">基于大模型的文档理解与语义检索框架</p>
|
||||
</div>
|
||||
|
||||
<div class="login-form">
|
||||
<t-form
|
||||
ref="formRef"
|
||||
:data="formData"
|
||||
:rules="formRules"
|
||||
@submit="handleLogin"
|
||||
layout="vertical"
|
||||
>
|
||||
<t-form-item label="邮箱" name="email">
|
||||
<t-input
|
||||
v-model="formData.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
type="email"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item label="密码" name="password">
|
||||
<t-input
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码(8-32位,包含字母和数字)"
|
||||
type="password"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-button
|
||||
type="submit"
|
||||
theme="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</t-button>
|
||||
</t-form>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div class="register-link">
|
||||
<span>还没有账号?</span>
|
||||
<a href="#" @click.prevent="toggleMode" class="register-btn">
|
||||
立即注册
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div class="register-card" v-if="isRegisterMode">
|
||||
<div class="login-header">
|
||||
<h1 class="login-title">创建账号</h1>
|
||||
<p class="login-subtitle">注册后系统将为您创建专属租户</p>
|
||||
</div>
|
||||
|
||||
<div class="login-form">
|
||||
<t-form
|
||||
ref="registerFormRef"
|
||||
:data="registerData"
|
||||
:rules="registerRules"
|
||||
@submit="handleRegister"
|
||||
layout="vertical"
|
||||
>
|
||||
<t-form-item label="用户名" name="username">
|
||||
<t-input
|
||||
v-model="registerData.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item label="邮箱" name="email">
|
||||
<t-input
|
||||
v-model="registerData.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
type="email"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item label="密码" name="password">
|
||||
<t-input
|
||||
v-model="registerData.password"
|
||||
placeholder="请输入密码(8-32位,包含字母和数字)"
|
||||
type="password"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item label="确认密码" name="confirmPassword">
|
||||
<t-input
|
||||
v-model="registerData.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
@keydown.enter="handleRegister"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-button
|
||||
type="submit"
|
||||
theme="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
>
|
||||
{{ loading ? '注册中...' : '注册' }}
|
||||
</t-button>
|
||||
</t-form>
|
||||
|
||||
<!-- 返回登录 -->
|
||||
<div class="register-link">
|
||||
<span>已有账号?</span>
|
||||
<a href="#" @click.prevent="toggleMode" class="register-btn">
|
||||
返回登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { login, register } from '@/api/auth'
|
||||
import { loadTestData, resetTestDataLoaded } from '@/api/test-data'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref()
|
||||
const registerFormRef = ref()
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const isRegisterMode = ref(false)
|
||||
|
||||
|
||||
// 登录表单数据
|
||||
const formData = reactive<{[key: string]: any}>({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// 注册表单数据
|
||||
const registerData = reactive<{[key: string]: any}>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 登录表单验证规则
|
||||
const formRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', type: 'error' },
|
||||
{ email: true, message: '请输入正确的邮箱格式', type: 'error' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', type: 'error' },
|
||||
{ min: 8, message: '密码至少8位', type: 'error' },
|
||||
{ max: 32, message: '密码不能超过32位', type: 'error' },
|
||||
{ pattern: /[a-zA-Z]/, message: '密码必须包含字母', type: 'error' },
|
||||
{ pattern: /\d/, message: '密码必须包含数字', type: 'error' }
|
||||
]
|
||||
}
|
||||
|
||||
// 注册表单验证规则
|
||||
const registerRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', type: 'error' },
|
||||
{ min: 2, message: '用户名至少2位', type: 'error' },
|
||||
{ max: 20, message: '用户名不能超过20位', type: 'error' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/,
|
||||
message: '用户名只能包含字母、数字、下划线和中文',
|
||||
type: 'error'
|
||||
}
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', type: 'error' },
|
||||
{ email: true, message: '请输入正确的邮箱格式', type: 'error' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', type: 'error' },
|
||||
{ min: 8, message: '密码至少8位', type: 'error' },
|
||||
{ max: 32, message: '密码不能超过32位', type: 'error' },
|
||||
{ pattern: /[a-zA-Z]/, message: '密码必须包含字母', type: 'error' },
|
||||
{ pattern: /\d/, message: '密码必须包含数字', type: 'error' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码', type: 'error' },
|
||||
{
|
||||
validator: (val: string) => val === registerData.password,
|
||||
message: '两次输入的密码不一致',
|
||||
type: 'error'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 切换登录/注册模式
|
||||
const toggleMode = () => {
|
||||
isRegisterMode.value = !isRegisterMode.value
|
||||
|
||||
Object.keys(registerData).forEach(key => {
|
||||
(registerData as any)[key] = ''
|
||||
})
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
const response = await login({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
// 保存用户信息和token
|
||||
if (response.user && response.tenant && response.token) {
|
||||
authStore.setUser({
|
||||
id: response.user.id || '',
|
||||
username: response.user.username || '',
|
||||
email: response.user.email || '',
|
||||
avatar: response.user.avatar,
|
||||
tenant_id: String(response.tenant.id) || '',
|
||||
created_at: response.user.created_at || new Date().toISOString(),
|
||||
updated_at: response.user.updated_at || new Date().toISOString()
|
||||
})
|
||||
authStore.setToken(response.token)
|
||||
if (response.refresh_token) {
|
||||
authStore.setRefreshToken(response.refresh_token)
|
||||
}
|
||||
authStore.setTenant({
|
||||
id: String(response.tenant.id) || '',
|
||||
name: response.tenant.name || '',
|
||||
api_key: response.tenant.api_key || '',
|
||||
owner_id: response.user.id || '',
|
||||
created_at: response.tenant.created_at || new Date().toISOString(),
|
||||
updated_at: response.tenant.updated_at || new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
MessagePlugin.success('登录成功!')
|
||||
|
||||
// 登录成功后先重置并加载一次测试数据,确保有KB可用
|
||||
try {
|
||||
resetTestDataLoaded()
|
||||
await loadTestData()
|
||||
} catch (_) {}
|
||||
|
||||
// 等待状态更新完成后再跳转
|
||||
await nextTick()
|
||||
router.replace('/platform/knowledgeBase')
|
||||
} else {
|
||||
MessagePlugin.error(response.message || '登录失败,请检查邮箱或密码')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('登录错误:', error)
|
||||
MessagePlugin.error(error.message || '登录失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async () => {
|
||||
try {
|
||||
const valid = await registerFormRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
const response = await register({
|
||||
username: registerData.username,
|
||||
email: registerData.email,
|
||||
password: registerData.password
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
MessagePlugin.success('注册成功!系统已为您创建专属租户,请登录使用')
|
||||
|
||||
// 切换到登录模式并填入邮箱
|
||||
isRegisterMode.value = false
|
||||
formData.email = registerData.email
|
||||
|
||||
// 清空注册表单
|
||||
Object.keys(registerData).forEach(key => {
|
||||
(registerData as any)[key] = ''
|
||||
})
|
||||
} else {
|
||||
MessagePlugin.error(response.message || '注册失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('注册错误:', error)
|
||||
MessagePlugin.error(error.message || '注册失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理忘记密码
|
||||
const handleForgotPassword = () => {
|
||||
MessagePlugin.info('忘记密码功能暂未开放,请联系管理员')
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
onMounted(() => {
|
||||
if (authStore.isLoggedIn) {
|
||||
router.replace('/platform/tenant/knowledge-bases')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-card,
|
||||
.register-card {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 16px 0 #0000000f, 0 20px 24px -2px #0000001a;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
animation: fadeInUp .28s ease-out both;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.logo {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.logo-img {
|
||||
width: 180px;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #000000e6;
|
||||
margin: 0 0 8px 0;
|
||||
font-family: "PingFang SC";
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 16px;
|
||||
color: #0000008c;
|
||||
margin: 0;
|
||||
font-family: "PingFang SC";
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
:deep(.t-form-item__label) {
|
||||
font-size: 14px;
|
||||
color: #000000e6;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
font-family: "PingFang SC";
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:deep(.t-input) {
|
||||
border: 1px solid #E7E7E7;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #07C05F;
|
||||
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #07C05F;
|
||||
}
|
||||
|
||||
.t-input__inner {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
font-family: "PingFang SC";
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.t-input__wrap {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-form-item__control) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 16px 0 24px 0;
|
||||
width: 100%;
|
||||
|
||||
:deep(.t-checkbox) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.t-checkbox__input {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-checkbox__label) {
|
||||
font-size: 14px;
|
||||
color: #00000099;
|
||||
font-family: "PingFang SC";
|
||||
line-height: 1.4;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
font-size: 14px;
|
||||
color: #07C05F;
|
||||
text-decoration: none;
|
||||
font-family: "PingFang SC";
|
||||
line-height: 1.4;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: "PingFang SC";
|
||||
margin: 16px 0 8px 0;
|
||||
|
||||
:deep(.t-button) {
|
||||
background-color: #07C05F;
|
||||
border-color: #07C05F;
|
||||
|
||||
&:hover {
|
||||
background-color: #06a855;
|
||||
border-color: #06a855;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #00000099;
|
||||
font-family: "PingFang SC";
|
||||
|
||||
.register-btn {
|
||||
color: #07C05F;
|
||||
text-decoration: none;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-card,
|
||||
.register-card {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 6px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -27,33 +27,7 @@ const sendMsg = (value: string) => {
|
||||
}
|
||||
|
||||
async function createNewSession(value: string) {
|
||||
// 从localStorage获取设置中的知识库ID
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
let knowledgeBaseId = "";
|
||||
|
||||
if (settingsStr) {
|
||||
try {
|
||||
const settings = JSON.parse(settingsStr);
|
||||
if (settings.knowledgeBaseId) {
|
||||
knowledgeBaseId = settings.knowledgeBaseId;
|
||||
createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => {
|
||||
if (res.data && res.data.id) {
|
||||
getTitle(res.data.id, value);
|
||||
} else {
|
||||
// 错误处理
|
||||
console.error("创建会话失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("创建会话出错:", error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("解析设置失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果设置中没有知识库ID,则使用测试数据
|
||||
// 使用测试数据获取知识库ID
|
||||
const testData = getTestData();
|
||||
if (!testData || testData.knowledge_bases.length === 0) {
|
||||
console.error("测试数据未初始化或不包含知识库");
|
||||
@@ -61,7 +35,7 @@ async function createNewSession(value: string) {
|
||||
}
|
||||
|
||||
// 使用第一个知识库ID
|
||||
knowledgeBaseId = testData.knowledge_bases[0].id;
|
||||
const knowledgeBaseId = testData.knowledge_bases[0].id;
|
||||
|
||||
createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => {
|
||||
if (res.data && res.data.id) {
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<span class="dot" />{{ s.label }}
|
||||
</li>
|
||||
</ul>
|
||||
<t-divider />
|
||||
<t-button size="small" variant="outline" theme="danger" block @click="handleLogout">
|
||||
退出登录
|
||||
</t-button>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="init-main">
|
||||
@@ -780,8 +784,10 @@ import {
|
||||
listOllamaModels,
|
||||
testEmbeddingModel
|
||||
} from '@/api/initialization';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
type TFormRef = {
|
||||
validate: (fields?: string[] | undefined) => Promise<true | any>;
|
||||
clearValidate?: (fields?: string | string[]) => void;
|
||||
@@ -956,6 +962,12 @@ const goToSection = (id: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
authStore.logout();
|
||||
router.replace('/login');
|
||||
};
|
||||
|
||||
// 监听滚动,高亮当前区块
|
||||
const onScroll = () => {
|
||||
const order = ['ollama','llm','embedding','rerank','multimodal','docsplit','submit'];
|
||||
@@ -2335,6 +2347,27 @@ const detectEmbeddingDimension = async () => {
|
||||
|
||||
<style lang="less" scoped>
|
||||
.initialization-container {
|
||||
padding: 20px 16px;
|
||||
background: linear-gradient(180deg, #f7faf9 0%, #f9fbfa 60%, #ffffff 100%);
|
||||
scroll-behavior: smooth;
|
||||
|
||||
.initialization-header {
|
||||
text-align: center;
|
||||
margin: 10px auto 18px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.init-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
@@ -2420,6 +2453,30 @@ const detectEmbeddingDimension = async () => {
|
||||
min-width: 0;
|
||||
max-width: 960px;
|
||||
}
|
||||
/* 统一分区卡片视觉 */
|
||||
.config-section {
|
||||
background: #fff;
|
||||
border: 1px solid #eef4f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 18px rgba(7, 192, 95, 0.04);
|
||||
padding: 16px;
|
||||
margin: 14px 0;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
color: #07c05f;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.ollama-summary-card {
|
||||
max-width: 100%;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
Reference in New Issue
Block a user