From 1160f8a5140795f65ffd6dd011fad8149ac352d4 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sun, 20 Apr 2025 10:26:43 +0800 Subject: [PATCH] update --- packages/frontend/src/locales/en.json | 4 + packages/frontend/src/locales/zh.json | 4 + .../frontend/src/stores/appearance.store.ts | 22 ++--- packages/frontend/src/stores/audit.store.ts | 4 +- packages/frontend/src/stores/auth.store.ts | 18 ++-- .../src/stores/commandHistory.store.ts | 10 +-- .../frontend/src/stores/connections.store.ts | 12 +-- packages/frontend/src/stores/layout.store.ts | 10 +-- .../src/stores/notifications.store.ts | 14 +-- packages/frontend/src/stores/proxies.store.ts | 10 +-- .../src/stores/quickCommands.store.ts | 12 +-- .../frontend/src/stores/settings.store.ts | 8 +- packages/frontend/src/stores/tags.store.ts | 10 +-- packages/frontend/src/utils/apiClient.ts | 88 +++++++++++++++++++ packages/frontend/src/views/SettingsView.vue | 15 ++-- packages/frontend/src/views/SetupView.vue | 4 +- 16 files changed, 171 insertions(+), 74 deletions(-) create mode 100644 packages/frontend/src/utils/apiClient.ts diff --git a/packages/frontend/src/locales/en.json b/packages/frontend/src/locales/en.json index db648cf..92e24ea 100644 --- a/packages/frontend/src/locales/en.json +++ b/packages/frontend/src/locales/en.json @@ -691,6 +691,7 @@ "configureFocusSwitch": "Configure Focus Switcher" }, "layout": { + "loading": "Loading...", "configure": "Configure Layout", "pane": { "connections": "Connections", @@ -703,6 +704,9 @@ "quickCommands": "Quick Commands" } }, + "header": { + "hide": "Hide" + }, "commandHistory": { "title": "Command History", "searchPlaceholder": "Search history...", diff --git a/packages/frontend/src/locales/zh.json b/packages/frontend/src/locales/zh.json index fb62e9f..829bdee 100644 --- a/packages/frontend/src/locales/zh.json +++ b/packages/frontend/src/locales/zh.json @@ -691,6 +691,7 @@ "configureFocusSwitch": "配置焦点切换器" }, "layout": { + "loading": "加载中...", "configure": "配置布局", "pane": { "connections": "连接列表", @@ -703,6 +704,9 @@ "quickCommands": "快捷指令" } }, + "header": { + "hide": "隐藏" + }, "commandHistory": { "title": "命令历史", "searchPlaceholder": "搜索历史记录...", diff --git a/packages/frontend/src/stores/appearance.store.ts b/packages/frontend/src/stores/appearance.store.ts index fc02b2e..8e6ee9f 100644 --- a/packages/frontend/src/stores/appearance.store.ts +++ b/packages/frontend/src/stores/appearance.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { ref, computed, watch, nextTick } from 'vue'; // 导入 nextTick import type { ITheme } from 'xterm'; import type { TerminalTheme } from '../../../backend/src/types/terminal-theme.types'; // 引用后端类型 @@ -89,8 +89,8 @@ export const useAppearanceStore = defineStore('appearance', () => { try { // 并行加载外观设置和主题列表 const [settingsResponse, themesResponse] = await Promise.all([ - axios.get('/api/v1/appearance'), - axios.get('/api/v1/terminal-themes') // 获取所有主题 + apiClient.get('/appearance'), // 使用 apiClient + apiClient.get('/terminal-themes') // 使用 apiClient ]); appearanceSettings.value = settingsResponse.data; allTerminalThemes.value = themesResponse.data; // 更新 allTerminalThemes @@ -137,7 +137,7 @@ export const useAppearanceStore = defineStore('appearance', () => { async function updateAppearanceSettings(updates: UpdateAppearanceDto) { try { // 移除预设主题闪烁修复逻辑,不再需要 - const response = await axios.put('/api/v1/appearance', updates); + const response = await apiClient.put('/appearance', updates); // 使用 apiClient // 使用后端返回的最新设置更新本地状态 appearanceSettings.value = response.data; console.log('[AppearanceStore] 外观设置已更新:', appearanceSettings.value); @@ -234,7 +234,7 @@ export const useAppearanceStore = defineStore('appearance', () => { */ async function createTerminalTheme(name: string, themeData: ITheme) { try { - await axios.post('/api/v1/terminal-themes', { name, themeData }); + await apiClient.post('/terminal-themes', { name, themeData }); // 使用 apiClient await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('创建终端主题失败:', err); @@ -250,7 +250,7 @@ export const useAppearanceStore = defineStore('appearance', () => { */ async function updateTerminalTheme(id: string, name: string, themeData: ITheme) { try { - await axios.put(`/api/v1/terminal-themes/${id}`, { name, themeData }); + await apiClient.put(`/terminal-themes/${id}`, { name, themeData }); // 使用 apiClient await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('更新终端主题失败:', err); @@ -264,7 +264,7 @@ export const useAppearanceStore = defineStore('appearance', () => { */ async function deleteTerminalTheme(id: string) { try { - await axios.delete(`/api/v1/terminal-themes/${id}`); + await apiClient.delete(`/terminal-themes/${id}`); // 使用 apiClient // 如果删除的是当前激活的主题,则切换回默认主题 ID // 需要将字符串 id 转换为数字进行比较 const idNum = parseInt(id, 10); @@ -294,7 +294,7 @@ export const useAppearanceStore = defineStore('appearance', () => { formData.append('name', name); } try { - await axios.post('/api/v1/terminal-themes/import', formData, { + await apiClient.post('/terminal-themes/import', formData, { // 使用 apiClient headers: { 'Content-Type': 'multipart/form-data' } }); await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 @@ -310,7 +310,7 @@ export const useAppearanceStore = defineStore('appearance', () => { */ async function exportTerminalTheme(id: string) { try { - const response = await axios.get(`/api/v1/terminal-themes/${id}/export`, { + const response = await apiClient.get(`/terminal-themes/${id}/export`, { // 使用 apiClient responseType: 'blob' // 重要:接收二进制数据 }); // 从响应头获取文件名 @@ -346,7 +346,7 @@ export const useAppearanceStore = defineStore('appearance', () => { const formData = new FormData(); formData.append('pageBackgroundFile', file); try { - const response = await axios.post<{ filePath: string }>('/api/v1/appearance/background/page', formData, { + const response = await apiClient.post<{ filePath: string }>('/appearance/background/page', formData, { // 使用 apiClient headers: { 'Content-Type': 'multipart/form-data' } }); // 更新本地状态 (虽然 updateAppearanceSettings 也会做,但这里立即反映) @@ -366,7 +366,7 @@ export const useAppearanceStore = defineStore('appearance', () => { const formData = new FormData(); formData.append('terminalBackgroundFile', file); try { - const response = await axios.post<{ filePath: string }>('/api/v1/appearance/background/terminal', formData, { + const response = await apiClient.post<{ filePath: string }>('/appearance/background/terminal', formData, { // 使用 apiClient headers: { 'Content-Type': 'multipart/form-data' } }); appearanceSettings.value.terminalBackgroundImage = response.data.filePath; diff --git a/packages/frontend/src/stores/audit.store.ts b/packages/frontend/src/stores/audit.store.ts index 566ed20..4d66230 100644 --- a/packages/frontend/src/stores/audit.store.ts +++ b/packages/frontend/src/stores/audit.store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { AuditLogEntry, AuditLogApiResponse, AuditLogActionType } from '../types/server.types'; export const useAuditLogStore = defineStore('auditLog', () => { @@ -26,7 +26,7 @@ export const useAuditLogStore = defineStore('auditLog', () => { // Remove undefined filter values Object.keys(params).forEach(key => params[key] === undefined && delete params[key]); - const response = await axios.get('/api/v1/audit-logs', { params }); + const response = await apiClient.get('/audit-logs', { params }); // 使用 apiClient logs.value = response.data.logs; totalLogs.value = response.data.total; } catch (err: any) { diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index 26b17bc..0945543 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import router from '../router'; // 引入 router 用于重定向 import { setLocale } from '../i18n'; // 导入 setLocale @@ -57,7 +57,7 @@ export const useAuthStore = defineStore('auth', { try { // 后端可能返回 user 或 requiresTwoFactor // 将完整的 payload (包含 rememberMe) 发送给后端 - const response = await axios.post<{ message: string; user?: UserInfo; requiresTwoFactor?: boolean }>('/api/v1/auth/login', payload); + const response = await apiClient.post<{ message: string; user?: UserInfo; requiresTwoFactor?: boolean }>('/auth/login', payload); // 使用 apiClient if (response.data.requiresTwoFactor) { // 需要 2FA 验证 @@ -100,7 +100,7 @@ export const useAuthStore = defineStore('auth', { this.isLoading = true; this.error = null; try { - const response = await axios.post<{ message: string; user: UserInfo }>('/api/v1/auth/login/2fa', { token }); + const response = await apiClient.post<{ message: string; user: UserInfo }>('/auth/login/2fa', { token }); // 使用 apiClient // 2FA 验证成功 this.isAuthenticated = true; this.user = response.data.user; @@ -130,7 +130,7 @@ export const useAuthStore = defineStore('auth', { this.loginRequires2FA = false; // 重置 2FA 状态 try { // 调用后端的登出 API - await axios.post('/api/v1/auth/logout'); + await apiClient.post('/auth/logout'); // 使用 apiClient // 清除本地状态 this.isAuthenticated = false; @@ -178,7 +178,7 @@ export const useAuthStore = defineStore('auth', { async checkAuthStatus() { this.isLoading = true; try { - const response = await axios.get<{ isAuthenticated: boolean; user: UserInfo }>('/api/v1/auth/status'); + const response = await apiClient.get<{ isAuthenticated: boolean; user: UserInfo }>('/auth/status'); // 使用 apiClient if (response.data.isAuthenticated && response.data.user) { this.isAuthenticated = true; this.user = response.data.user; // 更新用户信息,包含 isTwoFactorEnabled 和 language @@ -213,7 +213,7 @@ export const useAuthStore = defineStore('auth', { this.isLoading = true; this.error = null; try { - const response = await axios.put<{ message: string }>('/api/v1/auth/password', { + const response = await apiClient.put<{ message: string }>('/auth/password', { // 使用 apiClient currentPassword, newPassword, }); @@ -240,7 +240,7 @@ export const useAuthStore = defineStore('auth', { this.isLoading = true; this.error = null; try { - const response = await axios.get('/api/v1/settings/ip-blacklist', { + const response = await apiClient.get('/settings/ip-blacklist', { // 使用 apiClient params: { limit, offset } }); // 注意:这里需要将获取到的数据存储在 state 中, @@ -266,7 +266,7 @@ export const useAuthStore = defineStore('auth', { this.isLoading = true; this.error = null; try { - await axios.delete(`/api/v1/settings/ip-blacklist/${encodeURIComponent(ip)}`); + await apiClient.delete(`/settings/ip-blacklist/${encodeURIComponent(ip)}`); // 使用 apiClient console.log(`IP ${ip} 已从黑名单删除`); // 成功后需要重新获取列表或从本地 state 中移除 return true; @@ -284,7 +284,7 @@ export const useAuthStore = defineStore('auth', { async checkSetupStatus() { // 不需要设置 isLoading,这个检查应该在后台快速完成 try { - const response = await axios.get<{ needsSetup: boolean }>('/api/v1/auth/needs-setup'); + const response = await apiClient.get<{ needsSetup: boolean }>('/auth/needs-setup'); // 使用 apiClient this.needsSetup = response.data.needsSetup; console.log(`[AuthStore] Needs setup status: ${this.needsSetup}`); return this.needsSetup; // 返回状态给调用者 diff --git a/packages/frontend/src/stores/commandHistory.store.ts b/packages/frontend/src/stores/commandHistory.store.ts index ac2e149..f5743ad 100644 --- a/packages/frontend/src/stores/commandHistory.store.ts +++ b/packages/frontend/src/stores/commandHistory.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; // 假设项目使用 axios +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { ref, computed } from 'vue'; import { useUiNotificationsStore } from './uiNotifications.store'; // 用于显示通知 @@ -42,7 +42,7 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => { isLoading.value = true; error.value = null; try { - const response = await axios.get('/api/v1/command-history'); + const response = await apiClient.get('/command-history'); // 使用 apiClient // 后端返回的是按时间戳升序 (旧->新) // 前端需要按时间戳降序 (新->旧),所以反转数组 historyList.value = response.data.reverse(); @@ -62,7 +62,7 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => { return; // 不添加空命令 } try { - const response = await axios.post<{ id: number }>('/api/v1/command-history', { command: command.trim() }); + const response = await apiClient.post<{ id: number }>('/command-history', { command: command.trim() }); // 使用 apiClient // 添加成功后,重新获取列表以保证顺序和 ID 正确 // 或者,可以在本地模拟添加,但为了简单和一致性,重新获取更好 await fetchHistory(); @@ -77,7 +77,7 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => { // 删除单条历史记录 const deleteCommand = async (id: number) => { try { - await axios.delete(`/api/v1/command-history/${id}`); + await apiClient.delete(`/command-history/${id}`); // 使用 apiClient // 从本地列表中移除 const index = historyList.value.findIndex(entry => entry.id === id); if (index !== -1) { @@ -95,7 +95,7 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => { const clearAllHistory = async () => { // 可以在调用前添加确认逻辑 (例如在组件层) try { - await axios.delete('/api/v1/command-history'); + await apiClient.delete('/command-history'); // 使用 apiClient historyList.value = []; // 清空本地列表 uiNotificationsStore.showSuccess('所有历史记录已清空'); } catch (err: any) { diff --git a/packages/frontend/src/stores/connections.store.ts b/packages/frontend/src/stores/connections.store.ts index f48ad4d..65709ca 100644 --- a/packages/frontend/src/stores/connections.store.ts +++ b/packages/frontend/src/stores/connections.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; // 引入 axios +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient // 定义连接信息接口 (与后端对应,不含敏感信息) export interface ConnectionInfo { @@ -37,7 +37,7 @@ export const useConnectionsStore = defineStore('connections', { this.error = null; try { // 注意:axios 默认会携带 cookie,因此如果用户已登录,会话 cookie 会被发送 - const response = await axios.get('/api/v1/connections'); + const response = await apiClient.get('/connections'); // 使用 apiClient this.connections = response.data; } catch (err: any) { console.error('获取连接列表失败:', err); @@ -69,7 +69,7 @@ export const useConnectionsStore = defineStore('connections', { this.isLoading = true; this.error = null; try { - const response = await axios.post<{ message: string; connection: ConnectionInfo }>('/api/v1/connections', newConnectionData); + const response = await apiClient.post<{ message: string; connection: ConnectionInfo }>('/connections', newConnectionData); // 使用 apiClient // 添加成功后,将新连接添加到列表前面 (或重新获取整个列表) this.connections.unshift(response.data.connection); return true; // 表示成功 @@ -93,7 +93,7 @@ export const useConnectionsStore = defineStore('connections', { try { // 发送 PUT 请求到 /api/v1/connections/:id // 注意:后端 API 需要支持接收这些字段并进行更新 - const response = await axios.put<{ message: string; connection: ConnectionInfo }>(`/api/v1/connections/${connectionId}`, updatedData); + const response = await apiClient.put<{ message: string; connection: ConnectionInfo }>(`/connections/${connectionId}`, updatedData); // 使用 apiClient // 更新成功后,在列表中找到并更新对应的连接信息 const index = this.connections.findIndex(conn => conn.id === connectionId); @@ -124,7 +124,7 @@ export const useConnectionsStore = defineStore('connections', { this.error = null; try { // 发送 DELETE 请求到 /api/v1/connections/:id - await axios.delete(`/api/v1/connections/${connectionId}`); + await apiClient.delete(`/connections/${connectionId}`); // 使用 apiClient // 删除成功后,从本地列表中移除该连接 this.connections = this.connections.filter(conn => conn.id !== connectionId); @@ -148,7 +148,7 @@ export const useConnectionsStore = defineStore('connections', { // this.isLoading = true; // this.error = null; try { - const response = await axios.post<{ success: boolean; message: string }>(`/api/v1/connections/${connectionId}/test`); + const response = await apiClient.post<{ success: boolean; message: string }>(`/connections/${connectionId}/test`); // 使用 apiClient return { success: response.data.success, message: response.data.message }; } catch (err: any) { console.error(`测试连接 ${connectionId} 失败:`, err); diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index 91510f1..2f7efe3 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia'; import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'; // 导入 axios 用于 API 调用 -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient // 定义所有可用面板的名称 export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands'; @@ -116,7 +116,7 @@ export const useLayoutStore = defineStore('layout', () => { // 1. 尝试从后端加载 try { console.log('[Layout Store] Attempting to load layout from backend...'); - const response = await axios.get('/api/v1/settings/layout'); + const response = await apiClient.get('/settings/layout'); // 使用 apiClient if (response.data) { // TODO: 在这里添加对 response.data 的结构验证,确保它符合 LayoutNode 接口 layoutTree.value = response.data; @@ -217,7 +217,7 @@ export const useLayoutStore = defineStore('layout', () => { console.log('[Layout Store] Attempting to load header visibility from backend...'); try { // --- 调用后端 API (复用 nav-bar-visibility 接口) --- - const response = await axios.get<{ visible: boolean }>('/api/v1/settings/nav-bar-visibility'); + const response = await apiClient.get<{ visible: boolean }>('/settings/nav-bar-visibility'); // 使用 apiClient if (response && typeof response.data.visible === 'boolean') { isHeaderVisible.value = response.data.visible; console.log(`[Layout Store] Header visibility loaded from backend: ${isHeaderVisible.value}`); @@ -240,7 +240,7 @@ export const useLayoutStore = defineStore('layout', () => { try { // --- 调用后端 API (复用 nav-bar-visibility 接口) --- - await axios.put('/api/v1/settings/nav-bar-visibility', { visible: newValue }); + await apiClient.put('/settings/nav-bar-visibility', { visible: newValue }); // 使用 apiClient console.log('[Layout Store] Header visibility saved to backend.'); } catch (error) { console.error('[Layout Store] Failed to save header visibility to backend:', error); @@ -265,7 +265,7 @@ export const useLayoutStore = defineStore('layout', () => { // 1. 保存到后端 try { console.log('[Layout Store] Attempting to save layout to backend...'); - await axios.put('/api/v1/settings/layout', layoutTree.value); // 发送对象,后端会 stringify + await apiClient.put('/settings/layout', layoutTree.value); // 使用 apiClient console.log('[Layout Store] 布局已成功保存到后端。'); } catch (error) { console.error('[Layout Store] 保存布局到后端失败:', error); diff --git a/packages/frontend/src/stores/notifications.store.ts b/packages/frontend/src/stores/notifications.store.ts index d38190f..d24ab17 100644 --- a/packages/frontend/src/stores/notifications.store.ts +++ b/packages/frontend/src/stores/notifications.store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; -import axios from 'axios'; // Assuming axios is globally available or installed +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { NotificationSetting, NotificationSettingData, NotificationChannelType } from '../types/server.types'; // Import NotificationChannelType export const useNotificationsStore = defineStore('notifications', () => { @@ -12,7 +12,7 @@ export const useNotificationsStore = defineStore('notifications', () => { isLoading.value = true; error.value = null; try { - const response = await axios.get('/api/v1/notifications'); + const response = await apiClient.get('/notifications'); // 使用 apiClient settings.value = response.data; } catch (err: any) { console.error('Error fetching notification settings:', err); @@ -27,7 +27,7 @@ export const useNotificationsStore = defineStore('notifications', () => { isLoading.value = true; error.value = null; try { - const response = await axios.post('/api/v1/notifications', settingData); + const response = await apiClient.post('/notifications', settingData); // 使用 apiClient settings.value.push(response.data); return response.data; } catch (err: any) { @@ -43,7 +43,7 @@ export const useNotificationsStore = defineStore('notifications', () => { isLoading.value = true; error.value = null; try { - const response = await axios.put(`/api/v1/notifications/${id}`, settingData); + const response = await apiClient.put(`/notifications/${id}`, settingData); // 使用 apiClient const index = settings.value.findIndex(s => s.id === id); if (index !== -1) { settings.value[index] = response.data; @@ -65,7 +65,7 @@ export const useNotificationsStore = defineStore('notifications', () => { isLoading.value = true; error.value = null; try { - await axios.delete(`/api/v1/notifications/${id}`); + await apiClient.delete(`/notifications/${id}`); // 使用 apiClient settings.value = settings.value.filter(s => s.id !== id); return true; } catch (err: any) { @@ -83,7 +83,7 @@ export const useNotificationsStore = defineStore('notifications', () => { error.value = null; // Clear previous general errors try { // Send the config to test in the request body - const response = await axios.post<{ message: string }>(`/api/v1/notifications/${id}/test`, { config }); + const response = await apiClient.post<{ message: string }>(`/notifications/${id}/test`, { config }); // 使用 apiClient return { success: true, message: response.data.message || '测试成功' }; } catch (err: any) { console.error(`Error testing notification setting ${id}:`, err); @@ -99,7 +99,7 @@ export const useNotificationsStore = defineStore('notifications', () => { error.value = null; try { // Send the channel type and config in the request body - const response = await axios.post<{ message: string }>(`/api/v1/notifications/test-unsaved`, { channel_type: channelType, config }); + const response = await apiClient.post<{ message: string }>(`/notifications/test-unsaved`, { channel_type: channelType, config }); // 使用 apiClient return { success: true, message: response.data.message || '测试成功' }; } catch (err: any) { console.error(`Error testing unsaved notification setting:`, err); diff --git a/packages/frontend/src/stores/proxies.store.ts b/packages/frontend/src/stores/proxies.store.ts index 7057a61..bb64750 100644 --- a/packages/frontend/src/stores/proxies.store.ts +++ b/packages/frontend/src/stores/proxies.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient // 定义代理信息接口 (前端使用,不含密码) export interface ProxyInfo { @@ -33,7 +33,7 @@ export const useProxiesStore = defineStore('proxies', { this.isLoading = true; this.error = null; try { - const response = await axios.get('/api/v1/proxies'); + const response = await apiClient.get('/proxies'); // 使用 apiClient this.proxies = response.data; } catch (err: any) { console.error('获取代理列表失败:', err); @@ -59,7 +59,7 @@ export const useProxiesStore = defineStore('proxies', { this.isLoading = true; this.error = null; try { - const response = await axios.post<{ message: string; proxy: ProxyInfo }>('/api/v1/proxies', newProxyData); + const response = await apiClient.post<{ message: string; proxy: ProxyInfo }>('/proxies', newProxyData); // 使用 apiClient this.proxies.unshift(response.data.proxy); // 将新代理添加到列表开头 return true; // 成功 } catch (err: any) { @@ -82,7 +82,7 @@ export const useProxiesStore = defineStore('proxies', { this.isLoading = true; this.error = null; try { - const response = await axios.put<{ message: string; proxy: ProxyInfo }>(`/api/v1/proxies/${proxyId}`, updatedData); + const response = await apiClient.put<{ message: string; proxy: ProxyInfo }>(`/proxies/${proxyId}`, updatedData); // 使用 apiClient const index = this.proxies.findIndex(p => p.id === proxyId); if (index !== -1) { // 使用返回的更新后的信息替换旧信息 @@ -111,7 +111,7 @@ export const useProxiesStore = defineStore('proxies', { this.isLoading = true; this.error = null; try { - await axios.delete(`/api/v1/proxies/${proxyId}`); + await apiClient.delete(`/proxies/${proxyId}`); // 使用 apiClient this.proxies = this.proxies.filter(p => p.id !== proxyId); // 从列表中移除 return true; // 成功 } catch (err: any) { diff --git a/packages/frontend/src/stores/quickCommands.store.ts b/packages/frontend/src/stores/quickCommands.store.ts index e180bde..e2e6556 100644 --- a/packages/frontend/src/stores/quickCommands.store.ts +++ b/packages/frontend/src/stores/quickCommands.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { ref, computed } from 'vue'; import { useUiNotificationsStore } from './uiNotifications.store'; import type { QuickCommand } from '../../../backend/src/repositories/quick-commands.repository'; // 复用后端类型定义 @@ -58,7 +58,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => { isLoading.value = true; error.value = null; try { - const response = await axios.get('/api/v1/quick-commands', { + const response = await apiClient.get('/quick-commands', { // 使用 apiClient params: { sortBy: sortBy.value } // 将排序参数传递给后端 }); quickCommandsList.value = response.data; @@ -74,7 +74,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => { // 添加快捷指令 const addQuickCommand = async (name: string | null, command: string): Promise => { try { - await axios.post('/api/v1/quick-commands', { name, command }); + await apiClient.post('/quick-commands', { name, command }); // 使用 apiClient await fetchQuickCommands(); // 添加成功后刷新列表 uiNotificationsStore.showSuccess('快捷指令已添加'); return true; @@ -89,7 +89,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => { // 更新快捷指令 const updateQuickCommand = async (id: number, name: string | null, command: string): Promise => { try { - await axios.put(`/api/v1/quick-commands/${id}`, { name, command }); + await apiClient.put(`/quick-commands/${id}`, { name, command }); // 使用 apiClient await fetchQuickCommands(); // 更新成功后刷新列表 uiNotificationsStore.showSuccess('快捷指令已更新'); return true; @@ -104,7 +104,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => { // 删除快捷指令 const deleteQuickCommand = async (id: number) => { try { - await axios.delete(`/api/v1/quick-commands/${id}`); + await apiClient.delete(`/quick-commands/${id}`); // 使用 apiClient // 从本地列表中移除,避免重新请求 const index = quickCommandsList.value.findIndex(cmd => cmd.id === id); if (index !== -1) { @@ -121,7 +121,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => { // 增加使用次数 (调用 API,然后更新本地数据) const incrementUsage = async (id: number) => { try { - await axios.post(`/api/v1/quick-commands/${id}/increment-usage`); + await apiClient.post(`/quick-commands/${id}/increment-usage`); // 使用 apiClient // 更新本地计数,避免重新请求整个列表 const command = quickCommandsList.value.find(cmd => cmd.id === id); if (command) { diff --git a/packages/frontend/src/stores/settings.store.ts b/packages/frontend/src/stores/settings.store.ts index 40748f5..b7b4d2a 100644 --- a/packages/frontend/src/stores/settings.store.ts +++ b/packages/frontend/src/stores/settings.store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { ref, computed } from 'vue'; // 移除 watch import i18n, { setLocale, defaultLng } from '../i18n'; // Import i18n instance and setLocale // 移除 ITheme 和默认主题定义,这些移到 appearance.store.ts @@ -39,7 +39,7 @@ export const useSettingsStore = defineStore('settings', () => { try { console.log('[SettingsStore] 加载通用设置...'); - const response = await axios.get>('/api/v1/settings'); + const response = await apiClient.get>('/settings'); // 使用 apiClient settings.value = response.data; // Store fetched general settings console.log('[SettingsStore] 通用设置已加载:', settings.value); @@ -118,7 +118,7 @@ export const useSettingsStore = defineStore('settings', () => { try { // 注意:后端 controller 现在会过滤,但前端也做一层检查更好 - await axios.put('/api/v1/settings', { [key]: value }); + await apiClient.put('/settings', { [key]: value }); // 使用 apiClient // Update store state *after* successful API call settings.value = { ...settings.value, [key]: value }; @@ -164,7 +164,7 @@ export const useSettingsStore = defineStore('settings', () => { try { // 注意:后端 controller 现在会过滤,但前端也做一层检查更好 - await axios.put('/api/v1/settings', filteredUpdates); + await apiClient.put('/settings', filteredUpdates); // 使用 apiClient // Update store state *after* successful API call settings.value = { ...settings.value, ...filteredUpdates }; diff --git a/packages/frontend/src/stores/tags.store.ts b/packages/frontend/src/stores/tags.store.ts index cfaa2c8..cfec085 100644 --- a/packages/frontend/src/stores/tags.store.ts +++ b/packages/frontend/src/stores/tags.store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; -import axios from 'axios'; // 假设使用 axios 发送请求 +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient // 定义标签信息接口 export interface TagInfo { @@ -20,7 +20,7 @@ export const useTagsStore = defineStore('tags', () => { isLoading.value = true; error.value = null; try { - const response = await axios.get('/api/v1/tags'); + const response = await apiClient.get('/tags'); // 使用 apiClient 并移除 base URL tags.value = response.data; return true; } catch (err: any) { @@ -37,7 +37,7 @@ export const useTagsStore = defineStore('tags', () => { isLoading.value = true; error.value = null; try { - const response = await axios.post<{ message: string, tag: TagInfo }>('/api/v1/tags', { name }); + const response = await apiClient.post<{ message: string, tag: TagInfo }>('/tags', { name }); // 使用 apiClient 并移除 base URL // 添加成功后,重新获取列表以保证数据同步 (或者直接将新标签添加到 ref) await fetchTags(); // 简单起见,重新获取 // tags.value.push(response.data.tag); // 另一种方式 @@ -57,7 +57,7 @@ export const useTagsStore = defineStore('tags', () => { isLoading.value = true; error.value = null; try { - await axios.put(`/api/v1/tags/${id}`, { name }); + await apiClient.put(`/tags/${id}`, { name }); // 使用 apiClient 并移除 base URL // 更新成功后,重新获取列表 await fetchTags(); return true; @@ -75,7 +75,7 @@ export const useTagsStore = defineStore('tags', () => { isLoading.value = true; error.value = null; try { - await axios.delete(`/api/v1/tags/${id}`); + await apiClient.delete(`/tags/${id}`); // 使用 apiClient 并移除 base URL // 删除成功后,重新获取列表 await fetchTags(); return true; diff --git a/packages/frontend/src/utils/apiClient.ts b/packages/frontend/src/utils/apiClient.ts new file mode 100644 index 0000000..ff34c7b --- /dev/null +++ b/packages/frontend/src/utils/apiClient.ts @@ -0,0 +1,88 @@ +import axios from 'axios'; +import router from '../router'; // 引入 router 用于可能的重定向 +import { useAuthStore } from '../stores/auth.store'; // 引入 auth store 用于检查认证状态和登出 + +// 创建 axios 实例 +const apiClient = axios.create({ + baseURL: '/api/v1', // 设置基础URL + timeout: 10000, // 设置请求超时时间 + withCredentials: true, // 允许携带 cookie +}); + +// 请求拦截器 (可选,例如添加认证 Token) +apiClient.interceptors.request.use( + (config) => { + // 可以在这里添加逻辑,比如从 store 获取 token 并添加到请求头 + // const authStore = useAuthStore(); + // if (authStore.token) { + // config.headers.Authorization = `Bearer ${authStore.token}`; + // } + return config; + }, + (error) => { + // 处理请求错误 + console.error('Request error:', error); + return Promise.reject(error); + } +); + +// 响应拦截器 +apiClient.interceptors.response.use( + (response) => { + // 对响应数据做点什么 + return response; + }, + (error) => { + // 处理响应错误 + console.error('Response error:', error.response || error.message); + + if (error.response) { + const { status } = error.response; + const authStore = useAuthStore(); // 在需要时获取 store 实例 + + // 处理常见的 HTTP 错误状态码 + switch (status) { + case 401: // 未授权 + // 如果用户当前是认证状态,则可能是 session 过期或无效 + if (authStore.isAuthenticated) { + console.warn('Unauthorized access detected. Logging out.'); + // 调用 store 中的 logout 方法,它会处理状态重置和路由跳转 + authStore.logout(); + // 可以选择抛出错误或返回一个特定的值,防止后续代码执行 + return Promise.reject(new Error('Unauthorized, logging out.')); + } else { + // 如果用户本来就未认证,可能只是访问了需要登录的接口,暂时不强制跳转 + console.log('Unauthorized access to protected route.'); + } + break; + case 403: // 禁止访问 + // 可以显示一个权限不足的提示 + console.error('Forbidden access.'); + // alert('您没有权限执行此操作。'); // 或者使用更友好的通知组件 + break; + case 404: // 未找到 + console.error('Resource not found.'); + break; + case 500: // 服务器内部错误 + console.error('Internal server error.'); + // alert('服务器发生错误,请稍后重试。'); + break; + // 可以根据需要添加更多错误状态码的处理 + default: + console.error(`Unhandled error status: ${status}`); + } + } else if (error.request) { + // 请求已发出,但没有收到响应 (例如网络问题) + console.error('Network error or no response received:', error.request); + // alert('网络错误,请检查您的连接。'); + } else { + // 发送请求时出了点问题 + console.error('Error setting up request:', error.message); + } + + // 将错误继续抛出,以便调用方可以捕获并处理 + return Promise.reject(error); + } +); + +export default apiClient; \ No newline at end of file diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index f5e4da0..4694fe0 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -222,7 +222,8 @@ import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; // setLocale is handled by the store now -import axios from 'axios'; +import apiClient from '../utils/apiClient'; // 使用统一的 apiClient +import { isAxiosError } from 'axios'; // 单独导入 isAxiosError import { startRegistration } from '@simplewebauthn/browser'; const authStore = useAuthStore(); @@ -361,17 +362,17 @@ const handleRegisterPasskey = async () => { return; } try { - const optionsResponse = await axios.post('/api/v1/auth/passkey/register-options'); + const optionsResponse = await apiClient.post('/auth/passkey/register-options'); // 使用 apiClient const options = optionsResponse.data; let registrationResponse = await startRegistration(options); - await axios.post('/api/v1/auth/passkey/verify-registration', { registrationResponse, name: passkeyName.value }); + await apiClient.post('/auth/passkey/verify-registration', { registrationResponse, name: passkeyName.value }); // 使用 apiClient passkeyMessage.value = t('settings.passkey.success.registered'); passkeyName.value = ''; } catch (error: any) { console.error('Passkey 注册流程出错:', error); if (error.name === 'NotAllowedError') { passkeyError.value = t('settings.passkey.error.cancelled'); - } else if (axios.isAxiosError(error) && error.response) { + } else if (isAxiosError(error) && error.response) { // 使用导入的 isAxiosError passkeyError.value = t('settings.passkey.error.verificationFailed', { message: error.response.data.message || 'Server error' }); } else { passkeyError.value = t('settings.passkey.error.genericRegistration', { message: error.message || t('settings.passkey.error.unknown') }); @@ -427,7 +428,7 @@ const handleSetup2FA = async () => { twoFactorMessage.value = ''; twoFactorSuccess.value = false; twoFactorLoading.value = true; setupData.value = null; verificationCode.value = ''; try { - const response = await axios.post<{ secret: string; qrCodeUrl: string }>('/api/v1/auth/2fa/setup'); + const response = await apiClient.post<{ secret: string; qrCodeUrl: string }>('/auth/2fa/setup'); // 使用 apiClient setupData.value = response.data; } catch (error: any) { console.error('开始设置 2FA 失败:', error); @@ -440,7 +441,7 @@ const handleVerifyAndActivate2FA = async () => { } twoFactorMessage.value = ''; twoFactorSuccess.value = false; twoFactorLoading.value = true; try { - await axios.post('/api/v1/auth/2fa/verify', { token: verificationCode.value }); + await apiClient.post('/auth/2fa/verify', { token: verificationCode.value }); // 使用 apiClient twoFactorMessage.value = t('settings.twoFactor.success.activated'); twoFactorSuccess.value = true; twoFactorEnabled.value = true; setupData.value = null; verificationCode.value = ''; @@ -455,7 +456,7 @@ const handleDisable2FA = async () => { } twoFactorMessage.value = ''; twoFactorSuccess.value = false; twoFactorLoading.value = true; try { - await axios.delete('/api/v1/auth/2fa', { data: { password: disablePassword.value } }); + await apiClient.delete('/auth/2fa', { data: { password: disablePassword.value } }); // 使用 apiClient twoFactorMessage.value = t('settings.twoFactor.success.disabled'); twoFactorSuccess.value = true; twoFactorEnabled.value = false; disablePassword.value = ''; diff --git a/packages/frontend/src/views/SetupView.vue b/packages/frontend/src/views/SetupView.vue index 8158e9b..ff2d769 100644 --- a/packages/frontend/src/views/SetupView.vue +++ b/packages/frontend/src/views/SetupView.vue @@ -63,7 +63,7 @@