feat: 接入平台用户验证

This commit is contained in:
肖应宇 2026-03-09 13:51:15 +08:00
parent b6ee6c949b
commit ec96a424c4
7 changed files with 252 additions and 19 deletions

View File

@ -44,7 +44,8 @@ import ShortcutsModal from "@/components/modals/ShortcutsModal.vue";
import SettingsModal from "@/components/modals/SettingsModal.vue"; import SettingsModal from "@/components/modals/SettingsModal.vue";
import ConversationSettingsModal from "@/components/modals/ConversationSettingsModal.vue"; import ConversationSettingsModal from "@/components/modals/ConversationSettingsModal.vue";
import { Check, AlertCircle, Info } from "@/components/icons"; import { Check, AlertCircle, Info } from "@/components/icons";
import { useAuthStore } from "./stores/auth";
const authStore = useAuthStore();
// Stores // Stores
const chatStore = useChatStore(); const chatStore = useChatStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@ -126,6 +127,9 @@ useKeyboard(
// //
onMounted(() => { onMounted(() => {
authStore.init();
console.log(authStore.token);
// // // //
// if (chatStore.conversations.length === 0) { // if (chatStore.conversations.length === 0) {
// chatStore.createConversation(); // chatStore.createConversation();

View File

@ -215,20 +215,20 @@ const modelSelect = ref(localStorage.getItem("modelSelect") || "");
const currentModelId = ref(settingsStore.getSelectedModelId()); const currentModelId = ref(settingsStore.getSelectedModelId());
onMounted(() => { onMounted(() => {
chatApi.getModels().then((res: any) => { // chatApi.getModels().then((res: any) => {
availableModels.value = res; // availableModels.value = res;
// // //
const model = availableModels.value?.find( // const model = availableModels.value?.find(
(m: any) => m.id === currentModelId.value, // (m: any) => m.id === currentModelId.value,
); // );
if (model) { // if (model) {
modelSelect.value = model.name; // modelSelect.value = model.name;
} else if (availableModels.value.length > 0) { // } else if (availableModels.value.length > 0) {
modelSelect.value = availableModels.value[0].name; // modelSelect.value = availableModels.value[0].name;
currentModelId.value = availableModels.value[0].id; // currentModelId.value = availableModels.value[0].id;
} // }
localStorage.setItem("modelSelect", modelSelect.value); // localStorage.setItem("modelSelect", modelSelect.value);
}); // });
}); });
// //

View File

@ -409,10 +409,10 @@ const availableModels: any = ref([]);
const defaultModel: any = ref(localStorage.getItem("defaultModel")); const defaultModel: any = ref(localStorage.getItem("defaultModel"));
onMounted(() => { onMounted(() => {
chatApi.getModels().then((res: any) => { // chatApi.getModels().then((res: any) => {
availableModels.value = res; // availableModels.value = res;
if (!defaultModel.value) defaultModel.value = res[0].name; // if (!defaultModel.value) defaultModel.value = res[0].name;
}); // });
}); });
const activeTab = ref("appearance"); const activeTab = ref("appearance");

93
src/services/request.ts Normal file
View File

@ -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<Response> {
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<User[]>('/api/users');
*/
export async function apiRequestJson<T = unknown>(
url: string,
options: RequestInit = {}
): Promise<T> {
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<string, string> {
const token = getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}

122
src/stores/auth.ts Normal file
View File

@ -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<string | null>(null);
const user = ref<UserInfo | null>(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<UserInfo | null> {
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');
// 获取 tokenURL > 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<string, string> {
if (token.value) {
return { Authorization: `Bearer ${token.value}` };
}
return {};
}
// 初始化(不等待,让调用方通过 isInitialized 判断)
init();
return {
// 状态
token,
user,
isAuthenticated,
userId,
isInitialized,
// 方法
setUser,
getAuthHeader,
init,
};
});

View File

@ -146,3 +146,13 @@ export interface AIModel {
provider: string; provider: string;
icon?: string; icon?: string;
} }
// 用户信息
export interface UserInfo {
id: string;
username?: string;
nickname?: string;
email?: string;
avatar?: string;
[key: string]: unknown;
}

View File

@ -21,6 +21,10 @@ export default defineConfig({
target: "http://localhost:8000", // Python服务器端口 target: "http://localhost:8000", // Python服务器端口
changeOrigin: true, changeOrigin: true,
}, },
"/api/auth": {
target: "https://sxwz.xueai.art",
changeOrigin: true,
},
}, },
}, },
build: { build: {