diff --git a/index.html b/index.html index 8700dfc..5657928 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> - Crypto AI - 加密货币AI服务平台 + Crypto.AI - AI Agent for Web3 diff --git a/src/router/index.ts b/src/router/index.ts index ded0995..50d4b5e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' +import { useUserStore } from '../stores/user' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -18,13 +19,54 @@ const router = createRouter({ path: '/ai-agent', name: 'ai-agent', component: () => import('../views/AIAgentView.vue'), + meta: { requiresVIP: true }, }, { path: '/ai-feed', name: 'ai-feed', component: () => import('../views/AIFeedView.vue'), }, + { + path: '/login', + name: 'login', + component: () => import('../views/LoginView.vue'), + meta: { requiresGuest: true }, + }, + { + path: '/register', + name: 'register', + component: () => import('../views/RegisterView.vue'), + meta: { requiresGuest: true }, + }, ], }) +// 路由守卫 +router.beforeEach((to, from, next) => { + const userStore = useUserStore() + const isAuthenticated = userStore.isAuthenticated + const userInfo = userStore.userInfo + + // 如果页面需要未登录状态(如登录、注册页面) + if (to.meta.requiresGuest && isAuthenticated) { + next('/') + return + } + + // 如果页面需要VIP权限 + if (to.meta.requiresVIP) { + if (!isAuthenticated) { + // 未登录用户,可以直接访问页面,在页面内会显示登录提示 + next() + return + } else if (userInfo && userInfo.level < 1) { + // 已登录但不是VIP用户,可以直接访问页面,在页面内会显示升级提示 + next() + return + } + } + + next() +}) + export default router diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..180a8c2 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,99 @@ +import { useUserStore } from '../stores/user' + +// 根据环境选择API基础URL +const apiBaseUrl = + import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work' + +// 用户认证相关API +export const authApi = { + // 发送验证码 + async sendVerificationCode(mail: string) { + const response = await fetch(`${apiBaseUrl}/user/send-verification-code`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ mail }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.detail || `请求失败: ${response.status}`) + } + + return await response.json() + }, + + // 用户注册 + async register(data: { + mail: string + nickname: string + password: string + verification_code: string + }) { + const response = await fetch(`${apiBaseUrl}/user/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.detail || `请求失败: ${response.status}`) + } + + return await response.json() + }, + + // 用户登录 + async login(data: { mail: string; password: string }) { + const response = await fetch(`${apiBaseUrl}/user/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.detail || `请求失败: ${response.status}`) + } + + const authData = await response.json() + + // 使用Pinia存储登录信息 + const userStore = useUserStore() + userStore.setAuth(authData) + + return authData + }, +} + +// 创建带有认证头的请求函数 +export function createAuthenticatedFetch() { + const userStore = useUserStore() + + return async function authenticatedFetch(url: string, options: RequestInit = {}) { + const headers = { + ...options.headers, + Authorization: userStore.authHeader, + } + + const response = await fetch(url, { + ...options, + headers, + }) + + // 处理401错误(未授权) + if (response.status === 401) { + userStore.logout() + window.location.href = '/login' + throw new Error('登录已过期,请重新登录') + } + + return response + } +} diff --git a/src/stores/user.ts b/src/stores/user.ts new file mode 100644 index 0000000..5eaea4d --- /dev/null +++ b/src/stores/user.ts @@ -0,0 +1,68 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export interface UserInfo { + id: number + mail: string + nickname: string + level: number + create_time: string +} + +export interface AuthResponse { + access_token: string + token_type: string + expires_in: number + user_info: UserInfo +} + +export const useUserStore = defineStore('user', () => { + // 状态 + const accessToken = ref(localStorage.getItem('access_token')) + const userInfo = ref(null) + const isAuthenticated = computed(() => !!accessToken.value) + + // 从本地存储中恢复用户信息 + try { + const savedUserInfo = localStorage.getItem('user_info') + if (savedUserInfo) { + userInfo.value = JSON.parse(savedUserInfo) + } + } catch (error) { + console.error('Failed to parse user info from localStorage:', error) + } + + // 设置用户信息和令牌 + function setAuth(authResponse: AuthResponse) { + accessToken.value = authResponse.access_token + userInfo.value = authResponse.user_info + + // 保存到本地存储 + localStorage.setItem('access_token', authResponse.access_token) + localStorage.setItem('user_info', JSON.stringify(authResponse.user_info)) + } + + // 登出 + function logout() { + accessToken.value = null + userInfo.value = null + + // 清除本地存储 + localStorage.removeItem('access_token') + localStorage.removeItem('user_info') + } + + // 获取认证头 + const authHeader = computed(() => { + return accessToken.value ? `Bearer ${accessToken.value}` : '' + }) + + return { + accessToken, + userInfo, + isAuthenticated, + authHeader, + setAuth, + logout, + } +}) diff --git a/src/views/AIAgentView.vue b/src/views/AIAgentView.vue index 4f727d3..f87f051 100644 --- a/src/views/AIAgentView.vue +++ b/src/views/AIAgentView.vue @@ -1,5 +1,15 @@ @@ -100,70 +118,85 @@ onMounted(() => { -
- + + -
- {{ error }} - -
- -
-
暂无数据
- -
-
-
- +
@@ -582,5 +615,73 @@ onMounted(() => { .timestamp { font-size: 0.8rem; } + + .login-prompt-actions { + flex-direction: column; + } +} + +/* 登录提示样式 */ +.login-prompt { + display: flex; + justify-content: center; + align-items: center; + padding: 3rem 1rem; + width: 100%; +} + +.login-prompt-content { + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + padding: 2rem; + text-align: center; + max-width: 400px; + width: 100%; +} + +.login-prompt-content h2 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.login-prompt-content p { + color: var(--color-text-secondary); + margin-bottom: 1.5rem; +} + +.login-prompt-actions { + display: flex; + gap: 1rem; + justify-content: center; +} + +.login-button, +.register-button { + padding: 0.7rem 1.5rem; + border-radius: var(--border-radius); + font-weight: 600; + text-decoration: none; + transition: all 0.2s ease; +} + +.login-button { + background-color: #3355ff; + color: white; +} + +.login-button:hover { + background-color: #2244ee; +} + +.register-button { + background-color: transparent; + border: 1px solid var(--color-border); + color: var(--color-text-secondary); +} + +.register-button:hover { + background-color: var(--color-bg-elevated); + color: var(--color-text-primary); } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 0392036..a372a48 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,14 +1,91 @@ - + + + + +
+ +