This commit is contained in:
Baobhan Sith
2025-05-10 00:00:31 +08:00
parent 36afe3d5c2
commit c36e961426
29 changed files with 2268 additions and 41 deletions
@@ -0,0 +1,281 @@
<template>
<div class="suspended-ssh-sessions-view p-2 flex flex-col h-full">
<div class="view-header mb-2">
<div class="relative w-full">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-text-secondary"></i>
</span>
<input
type="text"
v-model="searchTerm"
:placeholder="$t('suspendedSshSessions.searchPlaceholder')"
class="w-full pl-10 pr-4 py-2 border border-border rounded-md bg-input text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
@input="filterSessions"
/>
</div>
<!-- 可选显示挂起会话总数 -->
<!-- <div class="text-sm text-gray-500 mt-1">
当前挂起会话总数: {{ filteredSessions.length }} / {{ allSuspendedSshSessions.length }}
</div> -->
</div>
<div class="session-list-container flex-grow overflow-y-auto">
<div v-if="isLoading" class="text-center p-4">
<i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
<p>{{ $t('suspendedSshSessions.loading') }}</p>
</div>
<div v-else-if="filteredSessions.length === 0 && !isLoading" class="text-center p-4">
<p>{{ $t('suspendedSshSessions.noResults') }}</p>
</div>
<ul v-else class="list-none p-0 m-0">
<li
v-for="session in filteredSessions"
:key="session.suspendSessionId"
class="session-item p-3 mb-2 border rounded-md bg-surface-ground"
:class="{ 'opacity-60': session.backendSshStatus === 'disconnected_by_backend' }"
>
<div class="flex justify-between items-center">
<div class="session-info flex-grow mr-2">
<div class="font-bold text-lg">
<span
v-if="!session.isEditingName"
class="cursor-pointer hover:text-primary"
:title="$t('suspendedSshSessions.tooltip.editName')"
@click="startEditingName(session)"
>
{{ session.customSuspendName || session.connectionName }}
</span>
<input
v-else
v-model="session.editingNameValue"
type="text"
class="text-lg font-bold w-full px-1 py-0.5 border border-primary rounded-md bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
autofocus
@blur="finishEditingName(session)"
@keydown.enter.prevent="finishEditingName(session)"
@keydown.esc.prevent="cancelEditingName(session)"
/>
</div>
<div class="text-sm text-muted-color">
{{ $t('suspendedSshSessions.label.originalConnection') }}: {{ session.connectionName }}
</div>
<div class="text-xs text-muted-color mt-1">
{{ $t('suspendedSshSessions.label.suspendedAt') }}: {{ formatDateTime(session.suspendStartTime) }}
</div>
<div
v-if="session.backendSshStatus === 'disconnected_by_backend' && session.disconnectionTimestamp"
class="text-xs text-orange-500 mt-1"
>
{{ $t('suspendedSshSessions.disconnectedAt', { time: formatDateTime(session.disconnectionTimestamp) }) }}
</div>
</div>
<div class="session-status-actions flex flex-col items-end space-y-2">
<span
:class="[
'px-2 py-1 text-xs font-semibold rounded-full',
session.backendSshStatus === 'hanging' ? 'bg-green-100 text-green-700 dark:bg-green-700 dark:text-green-100' : 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100'
]"
>
{{ session.backendSshStatus === 'hanging' ? $t('suspendedSshSessions.status.hanging') : $t('suspendedSshSessions.status.disconnected') }}
</span>
<div class="actions flex space-x-2">
<button
v-if="session.backendSshStatus === 'hanging'"
@click="resumeSession(session)"
:title="$t('suspendedSshSessions.action.resume')"
class="px-3 py-1.5 text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors duration-150 inline-flex items-center"
>
<i class="fas fa-play mr-1.5"></i>
{{ $t('suspendedSshSessions.action.resume') }}
</button>
<button
@click="removeSession(session)"
:title="$t('suspendedSshSessions.action.remove')"
class="px-3 py-1.5 text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-150 inline-flex items-center"
>
<i class="fas fa-trash-alt mr-1.5"></i>
{{ $t('suspendedSshSessions.action.remove') }}
</button>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; // +++ 导入 storeToRefs +++
// PrimeVue components (InputText, Button, Tag) are assumed to be globally registered
// based on the structure of other views like QuickCommandsView.vue
// and the nature of the 'Cannot find module' errors which might indicate
// they are not meant to be imported directly here if globally available.
// 假设 sessionStore 存在并且有以下类型和方法
import { useSessionStore } from '../stores/session.store'; // 使用真实的 store
import type { SuspendedSshSession } from '../types/ssh-suspend.types'; // 确保 SuspendedSshSession 类型从正确的位置导入
const { t } = useI18n();
// 模拟类型,实际应从 ssh-suspend.types.ts 导入 (保持这个类型扩展)
interface SuspendedSshSessionUIData extends SuspendedSshSession {
isEditingName?: boolean;
editingNameValue?: string;
}
// // 模拟 sessionStore (注释掉)
// const mockSessionStore = {
// suspendedSshSessions: ref<SuspendedSshSessionUIData[]>([
// // ... mock data ...
// ]),
// fetchSuspendedSshSessions: async () => {
// console.log('[SuspendedSshSessionsView] Requesting suspended SSH sessions...');
// // 模拟 API 调用延迟
// return new Promise(resolve => setTimeout(() => {
// mockSessionStore.suspendedSshSessions.value = [
// // ... mock data ...
// ];
// isLoading.value = false;
// console.log('[SuspendedSshSessionsView] Mock sessions loaded:', mockSessionStore.suspendedSshSessions.value);
// resolve(true);
// }, 1500));
// },
// resumeSshSession: async (suspendSessionId: string, newFrontendSessionId: string) => {
// console.log(`[SuspendedSshSessionsView] Action: resumeSshSession(${suspendSessionId}, ${newFrontendSessionId})`);
// alert(`模拟恢复会话: ${suspendSessionId}`);
// },
// terminateAndRemoveSshSession: async (suspendSessionId: string) => {
// console.log(`[SuspendedSshSessionsView] Action: terminateAndRemoveSshSession(${suspendSessionId})`);
// mockSessionStore.suspendedSshSessions.value = mockSessionStore.suspendedSshSessions.value.filter(s => s.suspendSessionId !== suspendSessionId);
// alert(`模拟终止并移除会话: ${suspendSessionId}`);
// },
// removeSshSessionEntry: async (suspendSessionId: string) => {
// console.log(`[SuspendedSshSessionsView] Action: removeSshSessionEntry(${suspendSessionId})`);
// mockSessionStore.suspendedSshSessions.value = mockSessionStore.suspendedSshSessions.value.filter(s => s.suspendSessionId !== suspendSessionId);
// alert(`模拟移除已断开会话条目: ${suspendSessionId}`);
// },
// editSshSessionName: async (suspendSessionId: string, newName: string) => {
// console.log(`[SuspendedSshSessionsView] Action: editSshSessionName(${suspendSessionId}, ${newName})`);
// const session = mockSessionStore.suspendedSshSessions.value.find(s => s.suspendSessionId === suspendSessionId);
// if (session) {
// session.customSuspendName = newName;
// }
// alert(`模拟编辑名称: ${suspendSessionId} -> ${newName}`);
// },
// };
const sessionStore = useSessionStore(); // 使用真实的 store
// const sessionStore = mockSessionStore; // 使用模拟 store (注释掉)
// +++ 使用 storeToRefs 获取响应式状态,并将 isLoadingSuspendedSessions 重命名为 isLoading +++
const { suspendedSshSessions: storeSuspendedSshSessions, isLoadingSuspendedSessions: isLoading } = storeToRefs(sessionStore);
const searchTerm = ref('');
// const isLoading = ref(true); // 现在从 store 的 isLoading 获取
const allSuspendedSshSessions = computed(() => storeSuspendedSshSessions.value.map((s: SuspendedSshSession) => ({ // 显式为 s 添加类型
...(s as SuspendedSshSessionUIData), // 断言为包含 UI 状态的类型
isEditingName: (s as SuspendedSshSessionUIData).isEditingName ?? false,
editingNameValue: (s as SuspendedSshSessionUIData).editingNameValue ?? s.customSuspendName ?? s.connectionName,
})));
const filteredSessions = computed(() => {
if (!searchTerm.value.trim()) {
return allSuspendedSshSessions.value; // allSuspendedSshSessions 已经是 .value 之后的结果
}
const lowerSearchTerm = searchTerm.value.toLowerCase();
return allSuspendedSshSessions.value.filter((session: SuspendedSshSessionUIData) => // 为 session 添加类型
(session.customSuspendName?.toLowerCase() || '').includes(lowerSearchTerm) ||
session.connectionName.toLowerCase().includes(lowerSearchTerm)
);
});
const filterSessions = () => {
// 计算属性会自动处理过滤
};
const formatDateTime = (isoString?: string) => {
if (!isoString) return t('time.unknown');
try {
return new Date(isoString).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
} catch (e) {
return t('time.invalidDate');
}
};
const startEditingName = (session: SuspendedSshSessionUIData) => {
// 确保同一时间只有一个会话处于编辑状态(可选优化)
allSuspendedSshSessions.value.forEach((s: SuspendedSshSessionUIData) => s.isEditingName = false); // 为 s 添加类型
session.isEditingName = true;
session.editingNameValue = session.customSuspendName || session.connectionName;
};
const finishEditingName = (session: SuspendedSshSessionUIData) => {
if (!session.isEditingName) return;
session.isEditingName = false;
const newName = session.editingNameValue?.trim();
// 仅当名称有变化且不为空时才提交
if (newName && newName !== (session.customSuspendName || session.connectionName)) {
sessionStore.editSshSessionName(session.suspendSessionId, newName);
} else {
// 如果名称未变或变为空,则恢复显示原始值或之前的自定义名
session.editingNameValue = session.customSuspendName || session.connectionName;
}
};
const cancelEditingName = (session: SuspendedSshSessionUIData) => {
session.isEditingName = false;
session.editingNameValue = session.customSuspendName || session.connectionName; // 恢复原值
};
const resumeSession = (session: SuspendedSshSessionUIData) => {
// 实际应用中,newFrontendSessionId 可能需要由 sessionStore 或其他服务生成
// const newFrontendSessionId = `new-session-${Date.now()}`; // newFrontendSessionId 由 action 内部生成
sessionStore.resumeSshSession(session.suspendSessionId); // +++ 只传递 suspendSessionId +++
};
const removeSession = (session: SuspendedSshSessionUIData) => {
if (session.backendSshStatus === 'hanging') {
sessionStore.terminateAndRemoveSshSession(session.suspendSessionId);
} else if (session.backendSshStatus === 'disconnected_by_backend') {
sessionStore.removeSshSessionEntry(session.suspendSessionId);
}
};
onMounted(async () => {
// isLoading.value = true; // storeIsLoading 会自动更新
await sessionStore.fetchSuspendedSshSessions();
// isLoading.value = false; // fetchSuspendedSshSessions 内部应更新 storeIsLoading
});
</script>
<style scoped>
.suspended-ssh-sessions-view {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
.session-item {
transition: background-color 0.2s ease-in-out;
}
.session-item:hover {
background-color: var(--surface-hover); /* PrimeVue hover color */
}
/* 保持与 QuickCommandsView 类似的简洁风格 */
.p-inputtext-sm {
padding: 0.375rem 0.5rem; /* 调整输入框大小 */
font-size: 0.875rem;
}
</style>