feat: Add Login Page

This commit is contained in:
wizardchen
2025-09-16 02:47:39 +08:00
parent 092b30af3e
commit 81bd2e6c2c
30 changed files with 2583 additions and 370 deletions

View File

@@ -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",

View 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验证失败'
}
}
}

View File

@@ -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}`);
}

View File

@@ -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 =

View File

@@ -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 });
});

View File

@@ -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`);
}

View File

@@ -53,3 +53,12 @@ export async function loadTestData(): Promise<boolean> {
return false;
}
}
/**
* 重置测试数据加载状态,在重新登录或需要强制刷新时调用
*/
export function resetTestDataLoaded() {
isTestDataLoaded = false;
// 清空已缓存的测试数据,确保下次调用会重新获取
setTestData(null);
}

View 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

View File

@@ -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}`);
}

View File

@@ -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
View 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
}
})

View File

@@ -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: ''

View File

@@ -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 }) });
}
);

View 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>

View File

@@ -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) {

View File

@@ -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;