From ccfb1d0e3ed03cab4aaba0aa1b4eb32662b16134 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:04:11 +0800 Subject: [PATCH] update --- REFACTOR_PLAN.md | 108 ++++++++++++++ packages/frontend/src/locales/en-US.json | 15 +- packages/frontend/src/locales/ja-JP.json | 15 +- packages/frontend/src/locales/zh-CN.json | 15 +- .../frontend/src/stores/settings.store.ts | 82 ++++++++--- packages/frontend/src/views/DashboardView.vue | 132 ++++++++++++------ 6 files changed, 301 insertions(+), 66 deletions(-) create mode 100644 REFACTOR_PLAN.md diff --git a/REFACTOR_PLAN.md b/REFACTOR_PLAN.md new file mode 100644 index 0000000..9aefa50 --- /dev/null +++ b/REFACTOR_PLAN.md @@ -0,0 +1,108 @@ +# 重构设置加载与存储机制计划 (Refactoring Plan for Settings Loading/Storage) + +**目标:** 重构 `SettingsView.vue` 和 `settings.store.ts` 中的设置加载与存储机制,以明确区分登录和未登录状态,整合浏览器默认设置,使用 `localStorage` 进行未登录状态的持久化,并实现由后端驱动的首次登录同步机制。 + +**阶段 1: 后端调整 (需要后端开发协调)** + +1. **`/settings` API (GET):** + * 用户已登录:返回该用户保存的设置。 + * 用户未登录:返回明确表示未登录的响应(例如 401 Unauthorized,或带有特定标志的 200 OK)。 +2. **`/settings` API (PUT):** + * 确保能接受并存储所有相关设置项,包括 `language` 和 `timezone`。 +3. **登录/认证接口:** + * 成功登录后,响应中应包含一个标志(例如 `isFirstLogin: true/false`)来表明这是否是用户的首次登录。 + +**阶段 2: 前端 `settings.store.ts` 重构** + +1. **状态 (State):** + * 保留 `settings = ref>({})`。 + * 添加 `isUsingLocalStorage = ref(false)` 来跟踪当前状态是否基于 localStorage(用于未登录用户)。 +2. **`loadInitialSettings` Action:** + * 注入 `authStore`。 + * 检查 `authStore.isAuthenticated`。 + * **如果未认证:** + * 设置 `isUsingLocalStorage.value = true`。 + * 尝试从 `localStorage` 加载设置 (键名: `nexus_guest_settings`)。 + * 如果 `localStorage` 数据存在且有效: + * 解析数据并与必要的硬编码默认值(如 UI 相关的)合并。 + * 更新 `settings.value`。 + * 如果 `localStorage` 数据不存在或无效: + * 检测浏览器语言 (`navigator.language`) 和时区 (`Intl.DateTimeFormat().resolvedOptions().timeZone`)。 + * 将这些值与硬编码默认值结合,形成初始的 `settings.value`。 + * 将这套初始设置保存到 `localStorage`。 + * 根据确定的语言设置 `i18n`。 + * **如果已认证:** + * 设置 `isUsingLocalStorage.value = false`。 + * 调用后端 `/settings` API (GET)。 + * 使用响应数据更新 `settings.value`。**移除** 此处补充默认值的逻辑。 + * 根据获取的语言设置 `i18n`。 + * 清除 `localStorage` 中的访客设置 (`localStorage.removeItem('nexus_guest_settings')`)。 +3. **`syncInitialSettingsOnLogin` Action:** + * 此 action 在用户**首次**成功登录后被外部调用。 + * 读取当前的 `settings.value`。 + * 调用 `updateMultipleSettings` 将这些设置推送到后端。 +4. **`updateSetting` & `updateMultipleSettings` Actions:** + * 检查 `authStore.isAuthenticated`。 + * **如果已认证:** + * 调用后端 `/settings` API (PUT)。 + * API 调用成功后更新 `settings.value`。 + * **如果未认证:** + * 更新 `settings.value` 中的相应键值。 + * 将整个更新后的 `settings.value` 对象保存回 `localStorage`。 + * **不**调用后端 API。 +5. **Getters:** 无需大的改动。 + +**阶段 3: 前端登录/认证逻辑调整** + +1. 登录 API 调用成功后: + * 检查响应中的 `isFirstLogin` 标志。 + * 如果 `isFirstLogin` 为 `true`: + * 先调用 `settingsStore.loadInitialSettings()`。 + * 然后调用 `settingsStore.syncInitialSettingsOnLogin()`。 + * 如果 `isFirstLogin` 为 `false`: + * 仅调用 `settingsStore.loadInitialSettings()`。 + +**阶段 4: 前端 `SettingsView.vue` 简化** + +1. 移除用于同步本地组件状态和 store 的复杂 `watch` 逻辑。直接依赖 store 状态。 +2. 确保所有保存操作都调用 `settingsStore.updateSetting` 或 `settingsStore.updateMultipleSettings`。 + +**Mermaid 流程图:** + +```mermaid +graph TD + subgraph Initialization [应用初始化] + A[应用加载] --> B{用户是否已认证?}; + B -- 是 --> C[API GET /settings 获取用户设置]; + B -- 否 --> D{从 localStorage 加载访客设置?}; + D -- 是 --> E[解析 localStorage 数据]; + D -- 否 --> F[检测浏览器语言/时区]; + F --> G[结合硬编码默认值]; + G --> H[保存初始设置到 localStorage]; + E --> I[合并 localStorage 数据与硬编码默认值]; + C --> J[更新 Pinia 状态 (来自后端)]; + I --> K[更新 Pinia 状态 (来自 localStorage)]; + H --> K; + J --> L[设置 i18n 语言]; + K --> L; + C --> CL[清除 localStorage 访客设置] + end + + subgraph Login [用户登录流程] + M[登录成功] --> N{后端返回 isFirstLogin?}; + N -- 是 --> O[调用 settingsStore.loadInitialSettings() *]; + O --> P[调用 settingsStore.syncInitialSettingsOnLogin()]; + P --> Q[API PUT /settings (同步初始设置)]; + N -- 否 --> R[调用 settingsStore.loadInitialSettings()]; + end + subgraph SettingsUpdate [设置更新流程] + S[用户在 UI 修改设置] --> T{用户是否已认证?}; + T -- 是 --> U[调用 settingsStore.updateSetting/updateMultiple]; + U --> V[API PUT /settings]; + V --> W[更新 Pinia 状态]; + T -- 否 --> X[调用 settingsStore.updateSetting/updateMultiple]; + X --> Y[更新 Pinia 状态]; + Y --> Z[保存到 localStorage]; + end + + note right of O *加载可能已修改的访客设置,准备同步 \ No newline at end of file diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 5bd9023..4b9294b 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -680,7 +680,9 @@ "width": "Width", "height": "Height", "reconnect": "Reconnect", - "retry": "Retry" + "retry": "Retry", + "sortAscending": "Ascending", + "sortDescending": "Descending" }, "layoutConfigurator": { "title": "Layout Configurator", @@ -927,7 +929,16 @@ "viewAllConnections": "View All Connections", "recentActivity": "Recent Activity", "noRecentActivity": "No recent activity records", - "viewFullAuditLog": "View Full Audit Log" + "viewFullAuditLog": "View Full Audit Log", + "connectionList": "Connection List", + "noConnections": "No connection records", + "sortOptions": { + "lastConnected": "Last Connected", + "name": "Name", + "type": "Type", + "updated": "Updated", + "created": "Created" + } }, "terminalTabBar": { "selectServerTitle": "Select server to connect" diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 7c43f69..3a9a03b 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -676,7 +676,9 @@ "collapse": "折りたたむ", "search": "検索", "all": "すべて", - "filter": "フィルター" + "filter": "フィルター", + "sortAscending": "昇順", + "sortDescending": "降順" }, "layoutConfigurator": { "title": "レイアウトマネージャー", @@ -897,7 +899,16 @@ "viewAllConnections": "すべての接続を表示", "recentActivity": "最近のアクティビティ", "noRecentActivity": "最近のアクティビティ記録はありません", - "viewFullAuditLog": "完全な監査ログを表示" + "viewFullAuditLog": "完全な監査ログを表示", + "connectionList": "接続リスト", + "noConnections": "接続記録がありません", + "sortOptions": { + "lastConnected": "最終接続", + "name": "名前", + "type": "タイプ", + "updated": "更新日時", + "created": "作成日時" + } }, "terminalTabBar": { "selectServerTitle": "接続するサーバーを選択" diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index e7788d6..19a9d61 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -681,7 +681,9 @@ "width": "宽度", "height": "高度", "reconnect": "重新连接", - "retry": "重试" + "retry": "重试", + "sortAscending": "升序", + "sortDescending": "降序" }, "layoutConfigurator": { "title": "布局管理器", @@ -930,7 +932,16 @@ "viewAllConnections": "查看所有连接", "recentActivity": "最近活动", "noRecentActivity": "没有最近活动记录", - "viewFullAuditLog": "查看完整审计日志" + "viewFullAuditLog": "查看完整审计日志", + "connectionList": "连接列表", + "noConnections": "没有连接记录", + "sortOptions": { + "lastConnected": "最近连接", + "name": "名称", + "type": "类型", + "updated": "修改时间", + "created": "创建时间" + } }, "terminalTabBar": { "selectServerTitle": "选择要连接的服务器" diff --git a/packages/frontend/src/stores/settings.store.ts b/packages/frontend/src/stores/settings.store.ts index d789a9f..76344fe 100644 --- a/packages/frontend/src/stores/settings.store.ts +++ b/packages/frontend/src/stores/settings.store.ts @@ -2,10 +2,13 @@ import { defineStore } from 'pinia'; import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { ref, computed } from 'vue'; // 移除 watch import i18n, { setLocale, defaultLng, availableLocales } from '../i18n'; // Import i18n instance, setLocale, defaultLng, and availableLocales -import type { PaneName } from './layout.store'; // +++ Import PaneName type +++ -import { useAuthStore } from './auth.store'; // <--- 导入 authStore -// Import CAPTCHA types from backend (adjust path if needed, assuming types are mirrored or shared) -// For now, let's assume they are available via a shared types definition or manually defined here +import type { PaneName } from './layout.store'; +import { useAuthStore } from './auth.store'; +import type { ConnectionInfo } from './connections.store'; + +export type SortField = keyof Pick; +export type SortOrder = 'asc' | 'desc'; + // Assuming manual definition for now if no shared types exist: type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'none'; interface CaptchaSettings { @@ -47,9 +50,10 @@ interface SettingsState { timezone?: string; // NEW: 时区设置 (e.g., 'Asia/Shanghai', 'UTC') rdpModalWidth?: string; // NEW: RDP 模态框宽度 rdpModalHeight?: string; // NEW: RDP 模态框高度 - ipBlacklistEnabled?: string; // <-- NEW: IP 黑名单启用状态 'true' or 'false' - // Add other general settings keys here as needed - [key: string]: string | undefined; // Allow other string settings + ipBlacklistEnabled?: string; + dashboardSortBy?: SortField; + dashboardSortOrder?: SortOrder; + [key: string]: string | undefined; } @@ -224,7 +228,14 @@ export const useSettingsStore = defineStore('settings', () => { settings.value.rdpModalWidth = '1064'; // 默认宽度 (1024 + 40 padding) } if (settings.value.rdpModalHeight === undefined) { - settings.value.rdpModalHeight = '858'; // 默认高度 (768 + chrome) + settings.value.rdpModalHeight = '858'; + } + + if (settings.value.dashboardSortBy === undefined) { + settings.value.dashboardSortBy = 'last_connected_at'; + } + if (settings.value.dashboardSortOrder === undefined) { + settings.value.dashboardSortOrder = 'desc'; } // --- 语言设置 --- @@ -306,10 +317,12 @@ export const useSettingsStore = defineStore('settings', () => { 'timezone', // NEW: 添加时区键 'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键 - 'ipBlacklistEnabled' // <-- NEW: 添加 IP 黑名单启用键 - ]; - if (!allowedKeys.includes(key)) { - console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); + 'ipBlacklistEnabled', + 'dashboardSortBy', + 'dashboardSortOrder' + ]; + if (!allowedKeys.includes(key)) { + console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); throw new Error(`不允许更新设置项 '${key}'`); } @@ -366,10 +379,12 @@ export const useSettingsStore = defineStore('settings', () => { 'timezone', // NEW: 添加时区键 'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键 - 'ipBlacklistEnabled' // <-- NEW: 添加 IP 黑名单启用键 - ]; - const filteredUpdates: Partial = {}; - let languageUpdate: string | undefined = undefined; // Use string type + 'ipBlacklistEnabled', + 'dashboardSortBy', + 'dashboardSortOrder' + ]; + const filteredUpdates: Partial = {}; + let languageUpdate: string | undefined = undefined; for (const key in updates) { if (allowedKeys.includes(key as keyof SettingsState)) { @@ -517,6 +532,18 @@ export const useSettingsStore = defineStore('settings', () => { isLoading.value = false; } } + + async function saveDashboardSortPreference(sortBy: SortField, sortOrder: SortOrder) { + try { + await updateMultipleSettings({ + dashboardSortBy: sortBy, + dashboardSortOrder: sortOrder, + }); + } catch (error) { + console.error('[SettingsStore] Failed to save dashboard sort preference:', error); + // Optionally show error to user + } + } // 移除外观相关 actions: saveCustomThemes, resetCustomThemes, toggleStyleCustomizer @@ -596,9 +623,19 @@ export const useSettingsStore = defineStore('settings', () => { }); // NEW: Getter for timezone setting - const timezone = computed(() => settings.value.timezone || 'UTC'); // Fallback to UTC - - // --- CAPTCHA Getters (Public Only) --- + const timezone = computed(() => settings.value.timezone || 'UTC'); + + const dashboardSortBy = computed((): SortField => { + const savedSortBy = settings.value.dashboardSortBy; + const validFields: SortField[] = ['created_at', 'last_connected_at', 'updated_at', 'name', 'type']; + return savedSortBy && validFields.includes(savedSortBy) ? savedSortBy : 'last_connected_at'; + }); + + const dashboardSortOrder = computed((): SortOrder => { + const savedSortOrder = settings.value.dashboardSortOrder; + return savedSortOrder === 'asc' || savedSortOrder === 'desc' ? savedSortOrder : 'desc'; + }); + const isCaptchaEnabled = computed(() => captchaSettings.value?.enabled ?? false); const captchaProvider = computed(() => captchaSettings.value?.provider ?? 'none'); const hcaptchaSiteKey = computed(() => captchaSettings.value?.hcaptchaSiteKey ?? ''); @@ -636,6 +673,9 @@ export const useSettingsStore = defineStore('settings', () => { updateSidebarPaneWidth, // +++ 暴露更新特定面板宽度的 action +++ updateFileManagerLayoutSettings, // +++ 暴露更新文件管理器布局的 action +++ commandInputSyncTarget, // +++ 暴露命令输入同步目标 getter +++ - timezone, // NEW: 暴露时区 getter + timezone, + dashboardSortBy, + dashboardSortOrder, + saveDashboardSortPreference, }; -}); + }); diff --git a/packages/frontend/src/views/DashboardView.vue b/packages/frontend/src/views/DashboardView.vue index ef18f9d..0264a26 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -1,12 +1,14 @@