diff --git a/src/App.vue b/src/App.vue index 0e7f799..f0605b3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -44,7 +44,8 @@ import ShortcutsModal from "@/components/modals/ShortcutsModal.vue"; import SettingsModal from "@/components/modals/SettingsModal.vue"; import ConversationSettingsModal from "@/components/modals/ConversationSettingsModal.vue"; import { Check, AlertCircle, Info } from "@/components/icons"; - +import { useAuthStore } from "./stores/auth"; +const authStore = useAuthStore(); // Stores const chatStore = useChatStore(); const settingsStore = useSettingsStore(); @@ -126,6 +127,9 @@ useKeyboard( // 初始化 onMounted(() => { + authStore.init(); + console.log(authStore.token); + // // 如果没有对话,创建一个 // if (chatStore.conversations.length === 0) { // chatStore.createConversation(); diff --git a/src/components/modals/ConversationSettingsModal.vue b/src/components/modals/ConversationSettingsModal.vue index 5d7bd31..4b17db0 100644 --- a/src/components/modals/ConversationSettingsModal.vue +++ b/src/components/modals/ConversationSettingsModal.vue @@ -215,20 +215,20 @@ const modelSelect = ref(localStorage.getItem("modelSelect") || ""); const currentModelId = ref(settingsStore.getSelectedModelId()); onMounted(() => { - chatApi.getModels().then((res: any) => { - availableModels.value = res; - // 初始化模型显示名称 - const model = availableModels.value?.find( - (m: any) => m.id === currentModelId.value, - ); - if (model) { - modelSelect.value = model.name; - } else if (availableModels.value.length > 0) { - modelSelect.value = availableModels.value[0].name; - currentModelId.value = availableModels.value[0].id; - } - localStorage.setItem("modelSelect", modelSelect.value); - }); + // chatApi.getModels().then((res: any) => { + // availableModels.value = res; + // // 初始化模型显示名称 + // const model = availableModels.value?.find( + // (m: any) => m.id === currentModelId.value, + // ); + // if (model) { + // modelSelect.value = model.name; + // } else if (availableModels.value.length > 0) { + // modelSelect.value = availableModels.value[0].name; + // currentModelId.value = availableModels.value[0].id; + // } + // localStorage.setItem("modelSelect", modelSelect.value); + // }); }); // 本地设置副本 diff --git a/src/components/modals/SettingsModal.vue b/src/components/modals/SettingsModal.vue index 07e3868..a44c063 100644 --- a/src/components/modals/SettingsModal.vue +++ b/src/components/modals/SettingsModal.vue @@ -409,10 +409,10 @@ const availableModels: any = ref([]); const defaultModel: any = ref(localStorage.getItem("defaultModel")); onMounted(() => { - chatApi.getModels().then((res: any) => { - availableModels.value = res; - if (!defaultModel.value) defaultModel.value = res[0].name; - }); + // chatApi.getModels().then((res: any) => { + // availableModels.value = res; + // if (!defaultModel.value) defaultModel.value = res[0].name; + // }); }); const activeTab = ref("appearance"); diff --git a/src/services/request.ts b/src/services/request.ts new file mode 100644 index 0000000..1ab67bf --- /dev/null +++ b/src/services/request.ts @@ -0,0 +1,93 @@ +/** + * 统一请求封装 + * + * 自动从 Pinia store 获取认证 token + */ +import { useAuthStore } from '@/stores/auth'; + +/** + * 获取认证 token(从 Pinia store) + */ +function getToken(): string | null { + const authStore = useAuthStore(); + return authStore.token; +} + +/** + * 统一的请求封装函数 + * + * @param url - 请求地址 + * @param options - fetch 选项 + * @returns Response 对象 + * + * @example + * // GET 请求 + * const response = await apiRequest('/api/users'); + * const data = await response.json(); + * + * // POST 请求 + * const response = await apiRequest('/api/users', { + * method: 'POST', + * body: JSON.stringify({ name: 'John' }) + * }); + */ +export async function apiRequest( + url: string, + options: RequestInit = {} +): Promise { + const token = getToken(); + + // 判断是否为 FormData,不设置 Content-Type 让浏览器自动处理 + const isFormData = options.body instanceof FormData; + + // 合并默认配置 + const config: RequestInit = { + ...options, + headers: { + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + ...options.headers, + }, + }; + + return fetch(url, config); +} + +/** + * JSON 请求封装(自动解析响应) + * + * @param url - 请求地址 + * @param options - fetch 选项 + * @returns 解析后的 JSON 数据 + * + * @example + * const users = await apiRequestJson('/api/users'); + */ +export async function apiRequestJson( + url: string, + options: RequestInit = {} +): Promise { + const response = await apiRequest(url, options); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || `HTTP ${response.status}`); + } + + return response.json(); +} + +/** + * 获取带认证的 headers + * 用于需要手动构建 headers 的场景 + */ +export function getAuthHeaders(): Record { + const token = getToken(); + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + return headers; +} \ No newline at end of file diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..9983066 --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,122 @@ +/** + * 用户认证状态管理 + */ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import type { UserInfo } from '@/types/chat'; + +// MARK: dev 默认 token(当 URL 无 token 参数时使用) +const DEV_DEFAULT_TOKEN = ''; + +// 认证接口返回格式 +interface AuthResponse { + code: string; + msg: string; + success: boolean; + timestamp: number; + data: UserInfo | null; +} + +// 认证接口 +const AUTH_CHECK_URL = '/api/auth/check/checkTokenRn'; + +export const useAuthStore = defineStore('auth', () => { + // 状态 + const token = ref(null); + const user = ref(null); + const isInitialized = ref(false); + + // 计算属性 + const isAuthenticated = computed(() => !!token.value); + const userId = computed(() => user.value?.username || null); // username 用于 OSS 路径和数据库 user_id + + /** + * 验证 token 并获取用户信息 + */ + async function checkToken(tokenToCheck: string): Promise { + try { + const response = await fetch(`${AUTH_CHECK_URL}/${tokenToCheck}`); + if (!response.ok) { + return null; + } + const data: AuthResponse = await response.json(); + + if (data.success && data.data) { + return data.data; + } + return null; + } catch (error) { + console.error('[Auth] Token 验证失败:', error); + return null; + } + } + + /** + * 初始化 - 从 URL 参数获取 token,验证后设置用户信息 + */ + async function init() { + const searchParams = new URLSearchParams(window.location.search); + const urlToken = searchParams.get('token'); + + // 获取 token:URL > localStorage > 默认值 + const tokenValue = urlToken + || localStorage.getItem('DEV_DEFAULT_TOKEN') + || DEV_DEFAULT_TOKEN; + + if (!tokenValue) { + isInitialized.value = true; + window.$toast?.('未登录,请先登录', 'error'); + return; + } + + // 验证 token + const userInfo = await checkToken(tokenValue); + + if (userInfo) { + window.$toast?.(`登录成功, 欢迎 ${userInfo.nickname || userInfo.username}`, 'success'); + + token.value = tokenValue; + user.value = userInfo; + } else { + // 验证失败,清空 + token.value = null; + user.value = null; + } + + isInitialized.value = true; + } + + /** + * 设置用户信息 + */ + function setUser(userInfo: UserInfo) { + user.value = userInfo; + } + + /** + * 获取认证 header + */ + function getAuthHeader(): Record { + if (token.value) { + return { Authorization: `Bearer ${token.value}` }; + } + return {}; + } + + // 初始化(不等待,让调用方通过 isInitialized 判断) + init(); + + return { + // 状态 + token, + user, + isAuthenticated, + userId, + isInitialized, + + // 方法 + setUser, + getAuthHeader, + init, + }; +}); \ No newline at end of file diff --git a/src/types/chat.ts b/src/types/chat.ts index a217b8b..a34c850 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -146,3 +146,13 @@ export interface AIModel { provider: string; icon?: string; } + +// 用户信息 +export interface UserInfo { + id: string; + username?: string; + nickname?: string; + email?: string; + avatar?: string; + [key: string]: unknown; +} diff --git a/vite.config.ts b/vite.config.ts index af95ead..ce319d6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,6 +21,10 @@ export default defineConfig({ target: "http://localhost:8000", // Python服务器端口 changeOrigin: true, }, + "/api/auth": { + target: "https://sxwz.xueai.art", + changeOrigin: true, + }, }, }, build: {