diff --git a/packages/backend/src/repositories/connection.repository.ts b/packages/backend/src/repositories/connection.repository.ts index a113807..3d3f71a 100644 --- a/packages/backend/src/repositories/connection.repository.ts +++ b/packages/backend/src/repositories/connection.repository.ts @@ -218,6 +218,26 @@ export const deleteConnection = async (id: number): Promise => { } }; +/** + * 更新指定连接的 last_connected_at 时间戳 + * @param id 连接 ID + * @param timestamp Unix 时间戳 (秒) + */ +export const updateLastConnected = async (id: number, timestamp: number): Promise => { + const sql = `UPDATE connections SET last_connected_at = ? WHERE id = ?`; + try { + const db = await getDbInstance(); + const result = await runDb(db, sql, [timestamp, id]); + if (result.changes === 0) { + console.warn(`[Repository] updateLastConnected: No connection found with ID ${id} to update.`); + } + return result.changes > 0; + } catch (err: any) { + console.error(`Repository: 更新连接 ${id} 的 last_connected_at 时出错:`, err.message); + throw new Error('更新上次连接时间失败'); + } +}; + /** * 更新连接的标签关联 (使用事务) * @param connectionId 连接 ID diff --git a/packages/backend/src/services/ssh.service.ts b/packages/backend/src/services/ssh.service.ts index bb86b66..30137ad 100644 --- a/packages/backend/src/services/ssh.service.ts +++ b/packages/backend/src/services/ssh.service.ts @@ -119,9 +119,21 @@ export const establishSshConnection = ( keepaliveCountMax: 10, }; - const readyHandler = () => { + const readyHandler = async () => { // 改为 async 函数 console.log(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 成功。`); sshClient.removeListener('error', errorHandler); // 成功后移除错误监听器 + + // --- 新增:更新 last_connected_at --- + try { + const currentTimeSeconds = Math.floor(Date.now() / 1000); + await ConnectionRepository.updateLastConnected(connDetails.id, currentTimeSeconds); + console.log(`SshService: 已更新连接 ${connDetails.id} 的 last_connected_at 为 ${currentTimeSeconds}`); + } catch (updateError) { + // 更新失败不应阻止连接成功,但需要记录错误 + console.error(`SshService: 更新连接 ${connDetails.id} 的 last_connected_at 失败:`, updateError); + } + // --- 结束新增 --- + resolve(sshClient); // 返回 Client 实例 }; diff --git a/packages/frontend/src/stores/audit.store.ts b/packages/frontend/src/stores/audit.store.ts index 64f6a75..69d7d44 100644 --- a/packages/frontend/src/stores/audit.store.ts +++ b/packages/frontend/src/stores/audit.store.ts @@ -11,28 +11,41 @@ export const useAuditLogStore = defineStore('auditLog', () => { const currentPage = ref(1); const logsPerPage = ref(50); // Default page size - // Updated fetchLogs to accept searchTerm and actionType directly - const fetchLogs = async ( - page: number = 1, - searchTerm?: string, - actionType?: AuditLogActionType | '' // Allow empty string from select - ) => { + // fetchLogs 现在接受一个选项对象作为参数 + const fetchLogs = async (options: { + page?: number; + limit?: number; // 新增 limit 参数 + searchTerm?: string; + actionType?: AuditLogActionType | ''; + sortOrder?: 'asc' | 'desc'; // 新增 sortOrder 参数 + } = {}) => { + const { + page = 1, + limit = logsPerPage.value, // 优先使用传入的 limit,否则使用 store 的默认值 + searchTerm, + actionType, + sortOrder + } = options; + isLoading.value = true; error.value = null; - currentPage.value = page; - const offset = (page - 1) * logsPerPage.value; + currentPage.value = page; // 仍然更新当前页码状态 + const offset = (page - 1) * limit; // offset 计算基于实际使用的 limit try { const params: Record = { - limit: logsPerPage.value, + limit: limit, // 使用实际的 limit offset: offset, - // Add new filter parameters if they have values + // 条件性添加其他参数 ...(searchTerm && { search: searchTerm }), ...(actionType && { action_type: actionType }), + ...(sortOrder && { sort_order: sortOrder }), // 添加 sort_order 参数 }; - // No need to remove undefined keys here as we conditionally add them const response = await apiClient.get('/audit-logs', { params }); // 使用 apiClient + // 注意:如果 fetchLogs 被用于分页,这里直接赋值 logs.value 可能不是最佳实践 + // 但对于仪表盘只获取少量最新日志的场景是可行的。 + // 如果需要支持加载更多,需要修改这里的逻辑为追加或替换。 logs.value = response.data.logs; totalLogs.value = response.data.total; } catch (err: any) { @@ -48,7 +61,7 @@ export const useAuditLogStore = defineStore('auditLog', () => { // Function to change page size and refetch const setLogsPerPage = (size: number) => { logsPerPage.value = size; - fetchLogs(1); // Reset to first page when size changes + fetchLogs({ page: 1 }); // 重置到第一页,使用默认 limit }; return { diff --git a/packages/frontend/src/views/AuditLogView.vue b/packages/frontend/src/views/AuditLogView.vue index f1315c0..c2fa599 100644 --- a/packages/frontend/src/views/AuditLogView.vue +++ b/packages/frontend/src/views/AuditLogView.vue @@ -142,11 +142,11 @@ const totalPages = computed(() => Math.ceil(totalLogs.value / logsPerPage.value) // Function to apply filters and fetch logs const applyFilters = () => { // Pass undefined if filter is empty, otherwise pass the value - store.fetchLogs( - 1, // Reset to page 1 when applying filters - searchTerm.value || undefined, - selectedActionType.value || undefined - ); + store.fetchLogs({ + page: 1, // Reset to page 1 when applying filters + searchTerm: searchTerm.value || undefined, + actionType: selectedActionType.value || undefined + }); }; // Removed watch for filters @@ -182,7 +182,12 @@ const formatDetails = (details: AuditLogEntry['details']): string => { const changePage = (page: number) => { if (page >= 1 && page <= totalPages.value && page !== currentPage.value) { - store.fetchLogs(page); + // Retain current filters when changing page + store.fetchLogs({ + page: page, + searchTerm: searchTerm.value || undefined, + actionType: selectedActionType.value || undefined + }); } }; diff --git a/packages/frontend/src/views/DashboardView.vue b/packages/frontend/src/views/DashboardView.vue index 90ab7a4..ff7b97a 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -23,12 +23,25 @@ const maxRecentLogs = 5; // --- 最近连接 --- const recentConnections = computed(() => { - // 过滤掉 last_connected_at 为 null 或 undefined 的连接 - const connected = connections.value.filter(c => c.last_connected_at); // 使用 last_connected_at - // 按 last_connected_at 降序排序 - connected.sort((a, b) => (b.last_connected_at ?? 0) - (a.last_connected_at ?? 0)); // 使用 last_connected_at - // 取前 N 条 - return connected.slice(0, maxRecentConnections); + console.log('[Dashboard] Raw connections from store:', JSON.parse(JSON.stringify(connections.value))); + + // 优先尝试按 last_connected_at 过滤和排序 + const connected = connections.value.filter(c => c.last_connected_at); + console.log('[Dashboard] Filtered connections (with last_connected_at):', JSON.parse(JSON.stringify(connected))); + + if (connected.length > 0) { + connected.sort((a, b) => (b.last_connected_at ?? 0) - (a.last_connected_at ?? 0)); + const result = connected.slice(0, maxRecentConnections); + console.log('[Dashboard] Final recent connections (using last_connected_at):', JSON.parse(JSON.stringify(result))); + return result; + } else { + // 如果没有带 last_connected_at 的连接,则按 updated_at 排序显示最近更新的 + console.log('[Dashboard] No connections with last_connected_at found. Falling back to sorting by updated_at.'); + const sortedByUpdate = [...connections.value].sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0)); + const result = sortedByUpdate.slice(0, maxRecentConnections); + console.log('[Dashboard] Final recent connections (fallback using updated_at):', JSON.parse(JSON.stringify(result))); + return result; + } }); // --- 最近活动 --- @@ -42,7 +55,9 @@ onMounted(async () => { // 如果 connections store 还没有加载过数据,则加载 if (connections.value.length === 0) { try { + console.log('[Dashboard] onMounted: Fetching connections...'); // 添加日志 await connectionsStore.fetchConnections(); + console.log('[Dashboard] onMounted: Connections fetched.'); // 添加日志 } catch (error) { console.error("加载连接列表失败:", error); // 可以在这里显示错误通知 @@ -50,8 +65,12 @@ onMounted(async () => { } // 加载最新的审计日志 try { - // 只需要加载少量日志用于摘要 - await auditLogStore.fetchLogs(1, maxRecentLogs, '', 'desc'); // 使用修正后的变量名 + // 只需要加载少量日志用于摘要,并按时间倒序 + await auditLogStore.fetchLogs({ + page: 1, + limit: maxRecentLogs, // 传递 limit + sortOrder: 'desc' // 传递 sortOrder + }); } catch (error) { console.error("加载审计日志失败:", error); // 可以在这里显示错误通知 @@ -70,7 +89,14 @@ const connectTo = (connection: ConnectionBase) => { // 使用 ConnectionBase 类 const formatRelativeTime = (dateString: string | undefined | null): string => { if (!dateString) return t('connections.status.never'); try { - const date = new Date(dateString); + // 将秒级时间戳转换为毫秒级 + const timestampInMs = Number(dateString) * 1000; + // 检查转换后的值是否有效 + if (isNaN(timestampInMs)) { + console.warn(`[Dashboard] Invalid timestamp received: ${dateString}`); + return dateString; // 返回原始值或错误提示 + } + const date = new Date(timestampInMs); const currentLocale = locale.value === 'zh' ? zhCN : enUS; return formatDistanceToNow(date, { addSuffix: true, locale: currentLocale }); } catch (e) {