feat: 完成修改挂起会话名称的功能

This commit is contained in:
Baobhan Sith
2025-05-10 09:44:53 +08:00
parent bf8124bb35
commit 05747a46e1
7 changed files with 192 additions and 180 deletions
@@ -8,7 +8,7 @@ import type {
SshSuspendResumeReqMessage,
SshSuspendTerminateReqMessage,
SshSuspendRemoveEntryReqMessage,
SshSuspendEditNameReqMessage,
// SshSuspendEditNameReqMessage, // Removed, using HTTP API
// S2C Payloads
SshMarkedForSuspendAckPayload,
SshUnmarkedForSuspendAckPayload, // +++ 新增导入 +++
@@ -17,10 +17,10 @@ import type {
SshOutputCachedChunkPayload,
SshSuspendTerminatedRespPayload,
SshSuspendEntryRemovedRespPayload,
SshSuspendNameEditedRespPayload,
// SshSuspendNameEditedRespPayload, // Removed, using HTTP API
SshSuspendAutoTerminatedNotifPayload,
} from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts
import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts
import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts // Re-add WsManagerInstance
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession
import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息
import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知
@@ -325,21 +325,43 @@ export const removeSshSessionEntry = async (suspendSessionId: string): Promise<v
};
/**
* 请求编辑挂起 SSH 会话的自定义名称
* 请求编辑挂起 SSH 会话的自定义名称 (通过 HTTP API)
* @param suspendSessionId 要编辑的挂起会话 ID
* @param customName 新的自定义名称
* @param newCustomName 新的自定义名称
*/
export const editSshSessionName = (suspendSessionId: string, customName: string): void => {
const wsManager = getActiveWsManager();
if (wsManager) {
const message: SshSuspendEditNameReqMessage = {
type: 'SSH_SUSPEND_EDIT_NAME',
payload: { suspendSessionId, customName },
};
wsManager.sendMessage(message);
console.log(`[${t('term.sshSuspend')}] 已发送 SSH_SUSPEND_EDIT_NAME_REQ (挂起 ID: ${suspendSessionId}, 名称: "${customName}")`);
} else {
console.warn(`[${t('term.sshSuspend')}] 编辑挂起名称失败 (挂起 ID: ${suspendSessionId}):无可用 WebSocket 连接。`);
export const editSshSessionName = async (suspendSessionId: string, newCustomName: string): Promise<void> => {
console.log(`[${t('term.sshSuspend')}] 请求通过 HTTP API 编辑挂起会话名称 (ID: ${suspendSessionId}, 新名称: "${newCustomName}")`);
const uiNotificationsStore = useUiNotificationsStore();
try {
// 假设后端 API 端点为 /api/ssh-suspend/name/:suspendSessionId
// 并且它接受一个包含 { customName: string } 的 PUT 请求体
// 并返回包含 { message: string, customName: string } 的成功响应
const response = await apiClient.put<{ message: string, customName: string }>(
`ssh-suspend/name/${suspendSessionId}`,
{ customName: newCustomName }
);
console.log(`[${t('term.sshSuspend')}] HTTP API 编辑名称 ${suspendSessionId} 成功:`, response.data);
// 更新前端状态
const session = suspendedSshSessions.value.find(s => s.suspendSessionId === suspendSessionId);
if (session) {
session.customSuspendName = response.data.customName; // 使用后端返回的名称确保一致性
uiNotificationsStore.addNotification({
type: 'success',
message: t('sshSuspend.notifications.nameEditedSuccess', { name: response.data.customName }),
});
} else {
// 如果会话在前端列表中找不到了(理论上不应该发生,因为是先找到再编辑的)
// 也可以选择重新获取列表
fetchSuspendedSshSessions();
}
} catch (error: any) {
console.error(`[${t('term.sshSuspend')}] 通过 HTTP API 编辑名称 ${suspendSessionId} 失败:`, error);
uiNotificationsStore.addNotification({
type: 'error',
message: t('sshSuspend.notifications.nameEditedError', { error: error.response?.data?.message || error.message || t('term.unknownError') }),
});
}
};
@@ -561,26 +583,7 @@ const handleSshSuspendEntryRemovedResp = (payload: SshSuspendEntryRemovedRespPay
}
};
const handleSshSuspendNameEditedResp = (payload: SshSuspendNameEditedRespPayload): void => {
const uiNotificationsStore = useUiNotificationsStore();
console.log(`[${t('term.sshSuspend')}] 接到 SSH_SUSPEND_NAME_EDITED_RESP:`, payload);
if (payload.success && payload.customName !== undefined) {
const session = suspendedSshSessions.value.find(s => s.suspendSessionId === payload.suspendSessionId);
if (session) {
session.customSuspendName = payload.customName;
uiNotificationsStore.addNotification({
type: 'success',
message: t('sshSuspend.notifications.nameEditedSuccess', { name: payload.customName }),
});
}
} else {
uiNotificationsStore.addNotification({
type: 'error',
message: t('sshSuspend.notifications.nameEditedError', { error: payload.error || t('term.unknownError') }),
});
console.error(`[${t('term.sshSuspend')}] 编辑挂起名称失败 (ID: ${payload.suspendSessionId}): ${payload.error}`);
}
};
// handleSshSuspendNameEditedResp removed as edit is now via HTTP
const handleSshSuspendAutoTerminatedNotif = (payload: SshSuspendAutoTerminatedNotifPayload): void => {
const uiNotificationsStore = useUiNotificationsStore();
@@ -621,10 +624,10 @@ export const registerSshSuspendHandlers = (wsManager: WsManagerInstance): void =
wsManager.onMessage('SSH_OUTPUT_CACHED_CHUNK', (p: MessagePayload) => handleSshOutputCachedChunk(p as SshOutputCachedChunkPayload));
wsManager.onMessage('SSH_SUSPEND_TERMINATED_RESP', (p: MessagePayload) => handleSshSuspendTerminatedResp(p as SshSuspendTerminatedRespPayload));
wsManager.onMessage('SSH_SUSPEND_ENTRY_REMOVED_RESP', (p: MessagePayload) => handleSshSuspendEntryRemovedResp(p as SshSuspendEntryRemovedRespPayload));
wsManager.onMessage('SSH_SUSPEND_NAME_EDITED_RESP', (p: MessagePayload) => handleSshSuspendNameEditedResp(p as SshSuspendNameEditedRespPayload));
// SSH_SUSPEND_NAME_EDITED_RESP handler removed
wsManager.onMessage('SSH_SUSPEND_AUTO_TERMINATED_NOTIF', (p: MessagePayload) => handleSshSuspendAutoTerminatedNotif(p as SshSuspendAutoTerminatedNotifPayload));
console.log(`[${t('term.sshSuspend')}] SSH 挂起模式的 WebSocket 消息处理器已注册。`);
console.log(`[${t('term.sshSuspend')}] SSH 挂起模式的 WebSocket 消息处理器已注册 (移除了名称编辑相关的处理器)`);
// 连接建立后,主动获取一次挂起列表
// 考虑:是否应该在这里做,或者在应用启动时做一次?
@@ -38,7 +38,7 @@
<div class="session-info flex-grow mr-2">
<div class="font-bold text-lg">
<span
v-if="!session.isEditingName"
v-if="editingSuspendSessionId !== session.suspendSessionId"
class="cursor-pointer hover:text-primary"
:title="$t('suspendedSshSessions.tooltip.editName')"
@click="startEditingName(session)"
@@ -47,13 +47,13 @@
</span>
<input
v-else
v-model="session.editingNameValue"
ref="nameInputRef"
v-model="currentEditingNameValue"
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)"
@blur="finishEditingName()"
@keydown.enter.prevent="finishEditingName()"
@keydown.esc.prevent="cancelEditingName()"
/>
</div>
<div class="text-sm text-muted-color">
@@ -107,87 +107,43 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed, nextTick, watch } from 'vue'; // +++ 导入 nextTick 和 watch +++
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 类型从正确的位置导入
import { storeToRefs } from 'pinia';
import { useSessionStore } from '../stores/session.store';
import type { SuspendedSshSession } from '../types/ssh-suspend.types';
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 sessionStore = useSessionStore();
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 editingSuspendSessionId = ref<string | null>(null);
const currentEditingNameValue = ref<string>('');
const nameInputRef = ref<HTMLInputElement | null>(null);
// +++ 监听编辑ID变化以聚焦输入框 +++
watch(editingSuspendSessionId, async (newId) => {
if (newId !== null) {
await nextTick(); // 确保DOM已更新,输入框已渲染
if (nameInputRef.value && typeof nameInputRef.value.focus === 'function') {
nameInputRef.value.focus();
// nameInputRef.value.select(); // 可选:如果希望选中所有文本
} else {
console.warn('[SuspendedSshSessionsView] Watcher: nameInputRef.value is not a focusable input after nextTick.');
}
}
});
// filteredSessions 现在直接基于 storeSuspendedSshSessions
const filteredSessions = computed(() => {
if (!searchTerm.value.trim()) {
return allSuspendedSshSessions.value; // allSuspendedSshSessions 已经是 .value 之后的结果
return storeSuspendedSshSessions.value;
}
const lowerSearchTerm = searchTerm.value.toLowerCase();
return allSuspendedSshSessions.value.filter((session: SuspendedSshSessionUIData) => // 为 session 添加类型
return storeSuspendedSshSessions.value.filter((session: SuspendedSshSession) =>
(session.customSuspendName?.toLowerCase() || '').includes(lowerSearchTerm) ||
session.connectionName.toLowerCase().includes(lowerSearchTerm)
);
@@ -201,44 +157,46 @@ 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',
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 startEditingName = (session: SuspendedSshSession) => { // async 不再需要,聚焦由 watcher 处理
editingSuspendSessionId.value = session.suspendSessionId;
currentEditingNameValue.value = session.customSuspendName || session.connectionName;
// 聚焦逻辑已移至 watcher
};
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 finishEditingName = () => {
if (editingSuspendSessionId.value === null) return;
const sessionId = editingSuspendSessionId.value;
const newName = currentEditingNameValue.value.trim();
const originalSession = storeSuspendedSshSessions.value.find(s => s.suspendSessionId === sessionId);
if (!originalSession) {
editingSuspendSessionId.value = null; // 重置状态
return;
}
editingSuspendSessionId.value = null; // 退出编辑模式
if (newName && newName !== (originalSession.customSuspendName || originalSession.connectionName)) {
sessionStore.editSshSessionName(sessionId, newName);
}
// 如果名称未变或为空,则无需操作,因为 currentEditingNameValue 不会持久化
};
const cancelEditingName = (session: SuspendedSshSessionUIData) => {
session.isEditingName = false;
session.editingNameValue = session.customSuspendName || session.connectionName; // 恢复原值
const cancelEditingName = () => {
editingSuspendSessionId.value = null;
// currentEditingNameValue 不需要显式重置,因为它会在下次 startEditingName 时被新值覆盖
};
const resumeSession = async (session: SuspendedSshSessionUIData) => {
const resumeSession = async (session: SuspendedSshSession) => { // 参数类型改为 SuspendedSshSession
console.log(`[SuspendedSshSessionsView] Attempting to resume session ID: ${session.suspendSessionId}, Name: ${session.customSuspendName || session.connectionName}`);
// 使用 JSON.parse(JSON.stringify()) 来记录会话对象的一个快照,避免在异步操作后因对象被修改而导致日志不准确
console.log('[SuspendedSshSessionsView] Session details snapshot:', JSON.parse(JSON.stringify(session)));
@@ -277,7 +235,7 @@ const resumeSession = async (session: SuspendedSshSessionUIData) => {
}
};
const removeSession = (session: SuspendedSshSessionUIData) => {
const removeSession = (session: SuspendedSshSession) => { // 参数类型改为 SuspendedSshSession
if (session.backendSshStatus === 'hanging') {
sessionStore.terminateAndRemoveSshSession(session.suspendSessionId);
} else if (session.backendSshStatus === 'disconnected_by_backend') {
@@ -286,9 +244,7 @@ const removeSession = (session: SuspendedSshSessionUIData) => {
};
onMounted(async () => {
// isLoading.value = true; // storeIsLoading 会自动更新
await sessionStore.fetchSuspendedSshSessions();
// isLoading.value = false; // fetchSuspendedSshSessions 内部应更新 storeIsLoading
});
</script>