From 80d5ab46ffe824f1f60cc9cdc8c29525cb826b49 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sun, 11 May 2025 18:23:00 +0800 Subject: [PATCH] update --- .../src/components/AddConnectionForm.vue | 1133 +---------------- .../components/AddConnectionFormAdvanced.vue | 83 ++ .../src/components/AddConnectionFormAuth.vue | 85 ++ .../components/AddConnectionFormBasicInfo.vue | 123 ++ .../src/composables/useAddConnectionForm.ts | 847 ++++++++++++ 5 files changed, 1192 insertions(+), 1079 deletions(-) create mode 100644 packages/frontend/src/components/AddConnectionFormAdvanced.vue create mode 100644 packages/frontend/src/components/AddConnectionFormAuth.vue create mode 100644 packages/frontend/src/components/AddConnectionFormBasicInfo.vue create mode 100644 packages/frontend/src/composables/useAddConnectionForm.ts diff --git a/packages/frontend/src/components/AddConnectionForm.vue b/packages/frontend/src/components/AddConnectionForm.vue index e74347e..d8653e0 100644 --- a/packages/frontend/src/components/AddConnectionForm.vue +++ b/packages/frontend/src/components/AddConnectionForm.vue @@ -1,923 +1,66 @@ - +

{{ t('connections.form.sectionScriptMode', '脚本模式') }}

diff --git a/packages/frontend/src/components/AddConnectionFormAdvanced.vue b/packages/frontend/src/components/AddConnectionFormAdvanced.vue new file mode 100644 index 0000000..834f716 --- /dev/null +++ b/packages/frontend/src/components/AddConnectionFormAdvanced.vue @@ -0,0 +1,83 @@ + + + \ No newline at end of file diff --git a/packages/frontend/src/components/AddConnectionFormAuth.vue b/packages/frontend/src/components/AddConnectionFormAuth.vue new file mode 100644 index 0000000..677cf63 --- /dev/null +++ b/packages/frontend/src/components/AddConnectionFormAuth.vue @@ -0,0 +1,85 @@ + + + \ No newline at end of file diff --git a/packages/frontend/src/components/AddConnectionFormBasicInfo.vue b/packages/frontend/src/components/AddConnectionFormBasicInfo.vue new file mode 100644 index 0000000..feb0c3a --- /dev/null +++ b/packages/frontend/src/components/AddConnectionFormBasicInfo.vue @@ -0,0 +1,123 @@ + + + \ No newline at end of file diff --git a/packages/frontend/src/composables/useAddConnectionForm.ts b/packages/frontend/src/composables/useAddConnectionForm.ts new file mode 100644 index 0000000..9e19bc2 --- /dev/null +++ b/packages/frontend/src/composables/useAddConnectionForm.ts @@ -0,0 +1,847 @@ +import { ref, reactive, watch, computed, onMounted, toRefs } from 'vue'; +import { storeToRefs } from 'pinia'; +import { useI18n } from 'vue-i18n'; +import apiClient from '../utils/apiClient'; +import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store'; +import { useProxiesStore } from '../stores/proxies.store'; +import { useTagsStore } from '../stores/tags.store'; +import { useSshKeysStore } from '../stores/sshKeys.store'; +import { useUiNotificationsStore } from '../stores/uiNotifications.store'; + +// Define Props interface based on the component's props +interface AddConnectionFormProps { + connectionToEdit: ConnectionInfo | null; +} + +// Define Emits type based on the component's emits +type AddConnectionFormEmits = { + (e: 'close'): void; + (e: 'connection-added'): void; + (e: 'connection-updated'): void; + (e: 'connection-deleted'): void; +}; + +export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddConnectionFormEmits) { + const { connectionToEdit } = toRefs(props); + + const { t } = useI18n(); + const connectionsStore = useConnectionsStore(); + const proxiesStore = useProxiesStore(); + const tagsStore = useTagsStore(); + const sshKeysStore = useSshKeysStore(); + const uiNotificationsStore = useUiNotificationsStore(); + + const { isLoading: isConnLoading, error: connStoreError } = storeToRefs(connectionsStore); + const { proxies, isLoading: isProxyLoading, error: proxyStoreError } = storeToRefs(proxiesStore); + const { tags, isLoading: isTagLoading, error: tagStoreError } = storeToRefs(tagsStore); + const { sshKeys, isLoading: isSshKeyLoading, error: sshKeyStoreError } = storeToRefs(sshKeysStore); + + // 表单数据模型 + const initialFormData = { + type: 'SSH' as 'SSH' | 'RDP' | 'VNC', // Use uppercase to match ConnectionInfo + name: '', + host: '', + port: 22, + username: '', + auth_method: 'password' as 'password' | 'key', // SSH specific + password: '', + private_key: '', // SSH specific (for direct input) - This field seems unused in the new logic, but kept for initialData consistency + passphrase: '', // SSH specific (for direct input) - This field seems unused, kept for consistency + selected_ssh_key_id: null as number | null, // +++ Add field for selected key ID +++ + proxy_id: null as number | null, + tag_ids: [] as number[], // 新增 tag_ids 字段 + notes: '', // 新增备注字段 + vncPassword: '', // VNC specific password + }; + const formData = reactive({ ...initialFormData }); + + const formError = ref(null); // 表单级别的错误信息 + // 合并所有 store 的加载和错误状态 + const isLoading = computed(() => isConnLoading.value || isProxyLoading.value || isTagLoading.value || isSshKeyLoading.value); // +++ Include SSH Key loading +++ + const storeError = computed(() => connStoreError.value || proxyStoreError.value || tagStoreError.value || sshKeyStoreError.value); // +++ Include SSH Key error +++ + + // 测试连接状态 + const testStatus = ref<'idle' | 'testing' | 'success' | 'error'>('idle'); + const testResult = ref(null); // 存储延迟或错误信息 + const testLatency = ref(null); // 单独存储延迟用于颜色计算 + + // Script Mode State + const isScriptModeActive = ref(false); + const scriptInputText = ref(''); + + // 计算属性判断是否为编辑模式 + const isEditMode = computed(() => !!connectionToEdit.value); + + // When switching to edit mode, disable script mode + watch(isEditMode, (editing) => { + if (editing) { + isScriptModeActive.value = false; + } + }); + + // 计算属性动态设置表单标题 + const formTitle = computed(() => { + return isEditMode.value ? t('connections.form.titleEdit') : t('connections.form.title'); + }); + + // 计算属性动态设置提交按钮文本 + const submitButtonText = computed(() => { + if (isLoading.value) { + return isEditMode.value ? t('connections.form.saving') : t('connections.form.adding'); + } + return isEditMode.value ? t('connections.form.confirmEdit') : t('connections.form.confirm'); + }); + + // 监听 prop 变化以填充或重置表单 + watch(connectionToEdit, (newVal) => { + formError.value = null; // 清除错误 + if (newVal) { + formData.type = newVal.type as 'SSH' | 'RDP' | 'VNC'; + formData.name = newVal.name; + formData.host = newVal.host; + formData.port = newVal.port; + formData.username = newVal.username; + formData.auth_method = newVal.auth_method; + formData.proxy_id = newVal.proxy_id ?? null; + formData.notes = newVal.notes ?? ''; + formData.tag_ids = newVal.tag_ids ? [...newVal.tag_ids] : []; + + if (newVal.type === 'SSH' && newVal.auth_method === 'key') { + formData.selected_ssh_key_id = newVal.ssh_key_id ?? null; + } else { + formData.selected_ssh_key_id = null; + } + + formData.password = ''; + formData.private_key = ''; + formData.passphrase = ''; + if (newVal.type !== 'VNC') { + formData.vncPassword = ''; + } else { + formData.vncPassword = ''; + } + } else { + Object.assign(formData, initialFormData); + formData.tag_ids = []; + formData.selected_ssh_key_id = null; + formData.notes = ''; + formData.vncPassword = ''; + } + }, { immediate: true }); + + // 组件挂载时获取代理、标签和 SSH 密钥列表 + onMounted(() => { + proxiesStore.fetchProxies(); + tagsStore.fetchTags(); + sshKeysStore.fetchSshKeys(); + }); + + // 监听连接类型变化,动态调整默认端口 + watch(() => formData.type, (newType) => { + if (newType === 'RDP') { + if (formData.port === 22 || formData.port === 5900 || formData.port === 5901) formData.port = 3389; + formData.auth_method = 'password'; + formData.selected_ssh_key_id = null; + } else if (newType === 'SSH') { + if (formData.port === 3389 || formData.port === 5900 || formData.port === 5901) formData.port = 22; + } else if (newType === 'VNC') { + if (formData.port === 22 || formData.port === 3389) formData.port = 5900; + formData.auth_method = 'password'; + formData.selected_ssh_key_id = null; + } + }); + + // Helper function to parse IP range + const parseIpRange = (ipRangeStr: string): string[] | { error: string } => { + if (!ipRangeStr.includes('~')) { + return { error: 'not_a_range' }; + } + const parts = ipRangeStr.split('~'); + if (parts.length !== 2) { + return { error: t('connections.form.errorInvalidIpRangeFormat', 'IP 范围格式应为 start_ip~end_ip') }; + } + + const [startIpStr, endIpStr] = parts.map(p => p.trim()); + + const ipRegex = /^((\d{1,3}\.){3})\d{1,3}$/; + if (!ipRegex.test(startIpStr) || !ipRegex.test(endIpStr)) { + return { error: t('connections.form.errorInvalidIpFormat', '起始或结束 IP 地址格式无效') }; + } + + const startIpParts = startIpStr.split('.'); + const endIpParts = endIpStr.split('.'); + + if (startIpParts.slice(0, 3).join('.') !== endIpParts.slice(0, 3).join('.')) { + return { error: t('connections.form.errorIpRangeNotSameSubnet', 'IP 范围必须在同一个C段子网中 (例如 1.2.3.x ~ 1.2.3.y)') }; + } + + const startSuffix = parseInt(startIpParts[3], 10); + const endSuffix = parseInt(endIpParts[3], 10); + + if (isNaN(startSuffix) || isNaN(endSuffix) || startSuffix < 0 || startSuffix > 255 || endSuffix < 0 || endSuffix > 255) { + return { error: t('connections.form.errorInvalidIpSuffix', 'IP 地址最后一段必须是 0-255 之间的数字') }; + } + + if (startSuffix > endSuffix) { + return { error: t('connections.form.errorIpRangeStartAfterEnd', 'IP 范围的起始 IP 不能大于结束 IP') }; + } + + const numIps = endSuffix - startSuffix + 1; + if (numIps <= 0) { + return { error: t('connections.form.errorIpRangeEmpty', 'IP 范围不能为空。') }; + } + + const baseIp = startIpParts.slice(0, 3).join('.'); + const ips: string[] = []; + for (let i = startSuffix; i <= endSuffix; i++) { + ips.push(`${baseIp}.${i}`); + } + return ips; + }; + + // Helper function to parse a single script line + const parseScriptLine = (line: string): { type: 'SSH' | 'RDP' | 'VNC' | null, userHostPort: string, name: string | null, password: string | null, keyName: string | null, tags: string[], note: string | null, error?: string } => { + const firstSpaceIndex = line.indexOf(' '); + let userHostPortPart = ''; + let remainingLine = line; + + if (firstSpaceIndex !== -1) { + userHostPortPart = line.substring(0, firstSpaceIndex); + remainingLine = line.substring(firstSpaceIndex + 1).trim(); + } else { + userHostPortPart = line; + remainingLine = ''; + } + + if (!userHostPortPart) return { type: null, userHostPort: '', name: null, password: null, keyName: null, tags: [], note: null, error: t('connections.form.scriptErrorMissingHost', '缺少 user@host:port 部分') }; + + let type: 'SSH' | 'RDP' | 'VNC' | null = 'SSH'; + let name: string | null = null; + let password: string | null = null; + let keyName: string | null = null; + const tags: string[] = []; + let note: string | null = null; + let currentArg: string | null = null; + let noteParts: string[] = []; + + const argRegex = /(-[^=\s]+)(?:\s+("(?:\\"|[^"])*"|[^-\s][^\s]*))?/g; + let match; + let lastIndex = 0; + + while ((match = argRegex.exec(remainingLine)) !== null) { + const arg = match[1]; + let value = match[2] || ''; + + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1).replace(/\\"/g, '"'); + } + + if (noteParts.length > 0 && currentArg === '-note') { + note = noteParts.join(' '); + noteParts = []; + } + + currentArg = arg; + + if (arg === '-tags') { + // Tags handled later + } else if (arg === '-note') { + // Note handled later + } + + if (value) { + switch (arg) { + case '-type': + const upperType = value.toUpperCase(); + if (upperType === 'SSH' || upperType === 'RDP' || upperType === 'VNC') { + type = upperType as 'SSH' | 'RDP' | 'VNC'; + } else { + return { type: null, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorInvalidType', { type: value }) }; + } + currentArg = null; + break; + case '-name': + name = value; + currentArg = null; + break; + case '-p': + password = value; + currentArg = null; + break; + case '-k': + keyName = value; + currentArg = null; + break; + case '-tags': + tags.push(value); + break; + case '-note': + noteParts.push(value); + break; + default: + if (currentArg === '-note') { + noteParts.push(value); + } else { + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorUnknownArg', { arg: currentArg }) }; + } + } + } + lastIndex = argRegex.lastIndex; + } + + const remainingPart = remainingLine.substring(lastIndex).trim(); + if (remainingPart) { + if (currentArg === '-tags') { + const tagRegex = /("(?:\\"|[^"])*"|[^\s"]+)/g; + let tagMatch; + while ((tagMatch = tagRegex.exec(remainingPart)) !== null) { + let tag = tagMatch[1]; + if (tag.startsWith('"') && tag.endsWith('"')) { + tag = tag.slice(1, -1).replace(/\\"/g, '"'); + } + tags.push(tag); + } + } else if (currentArg === '-note') { + noteParts.push(remainingPart); + } else { + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorUnexpectedToken', { token: remainingPart }) }; + } + } + + if (noteParts.length > 0 && currentArg === '-note') { + note = noteParts.join(' '); + } + + const userHostPortRegex = /^[^@]+@[^:]+(:[0-9]+)?$/; + if (!userHostPortRegex.test(userHostPortPart)) { + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorInvalidUserHostPort', { part: userHostPortPart })}; + } + + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note }; + }; + + // 处理表单提交 + const handleScriptModeSubmit = async () => { + const lines = scriptInputText.value.split('\n').filter(line => line.trim() !== ''); + + if (lines.length === 0) { + uiNotificationsStore.showError(t('connections.form.scriptModeEmpty', '脚本输入不能为空。')); + return; + } + + let allConnectionsValid = true; + const connectionsToAdd = []; + + for (const line of lines) { + const parsed = parseScriptLine(line); + if (parsed.error) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInLine', { line, error: parsed.error })); + allConnectionsValid = false; + break; + } + + if (!parsed.type) { + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingType', { line })); + allConnectionsValid = false; + break; + } + + const [userHost, portStr] = parsed.userHostPort.split(':'); + const [username, host] = userHost.split('@'); + const port = portStr ? parseInt(portStr, 10) : (parsed.type === 'RDP' ? 3389 : (parsed.type === 'VNC' ? 5900 : 22)); + + if (!username || !host) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInvalidUserHostFormat', { line })); + allConnectionsValid = false; + break; + } + if (isNaN(port) || port <= 0 || port > 65535) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInvalidPort', { line, port: portStr || (parsed.type === 'RDP' ? '3389' : (parsed.type === 'VNC' ? '5900' : '22')) })); + allConnectionsValid = false; + break; + } + + const connectionData: any = { + type: parsed.type, + name: parsed.name || `${username}@${host}`, + host, + port, + username, + notes: parsed.note || '', + tag_names: parsed.tags, + }; + + if (parsed.type === 'SSH') { + connectionData.auth_method = parsed.keyName ? 'key' : 'password'; + if (connectionData.auth_method === 'password') { + if (!parsed.password) { + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingPasswordForSsh', { line })); + allConnectionsValid = false; + break; + } + connectionData.password = parsed.password; + } else { + if (!parsed.keyName) { + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingKeyNameForSsh', { line })); + allConnectionsValid = false; + break; + } + connectionData.ssh_key_name = parsed.keyName; + } + } else if (parsed.type === 'RDP' || parsed.type === 'VNC') { + if (!parsed.password) { + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingPasswordForType', { line, type: parsed.type })); + allConnectionsValid = false; + break; + } + connectionData.password = parsed.password; + } + connectionsToAdd.push(connectionData); + } + + if (!allConnectionsValid || connectionsToAdd.length === 0) { + if (connectionsToAdd.length > 0 && !allConnectionsValid) { + // Errors were already shown + } else if (lines.length > 0 && connectionsToAdd.length === 0 && allConnectionsValid) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInternal', '内部解析错误。')); + } + return; + } + + const fullyProcessedConnections = []; + let resolutionErrorOccurred = false; + + for (const connData of connectionsToAdd) { + if (connData.tag_names && connData.tag_names.length > 0) { + const tagIds = []; + for (const tagName of connData.tag_names) { + const foundTag = tags.value.find(t_ => t_.name === tagName); // Renamed t to t_ to avoid conflict + if (foundTag) { + tagIds.push(foundTag.id); + } else { + uiNotificationsStore.showError(t('connections.form.scriptErrorTagNotFound', { tagName })); + resolutionErrorOccurred = true; + break; + } + } + if (resolutionErrorOccurred) break; + connData.tag_ids = tagIds; + } else { + connData.tag_ids = []; + } + delete connData.tag_names; + + if (connData.type === 'SSH' && connData.auth_method === 'key' && connData.ssh_key_name) { + const foundKey = sshKeys.value.find(k => k.name === connData.ssh_key_name); + if (foundKey) { + connData.ssh_key_id = foundKey.id; + } else { + uiNotificationsStore.showError(t('connections.form.scriptErrorSshKeyNotFound', { keyName: connData.ssh_key_name })); + resolutionErrorOccurred = true; + break; + } + delete connData.ssh_key_name; + } + + if (connData.type !== 'SSH' || connData.auth_method !== 'key') delete connData.ssh_key_id; + if (connData.type === 'SSH' && connData.auth_method === 'key') delete connData.password; + if (connData.type !== 'SSH') delete connData.auth_method; + + fullyProcessedConnections.push(connData); + } + + if (resolutionErrorOccurred || (fullyProcessedConnections.length === 0 && lines.length > 0)) { + if (!resolutionErrorOccurred && lines.length > 0 && fullyProcessedConnections.length === 0) { + uiNotificationsStore.showError(t('connections.form.scriptErrorNothingToProcess', '没有可处理的有效连接数据。')); + } + return; + } + + if (fullyProcessedConnections.length === 0) { + return; + } + + uiNotificationsStore.showInfo(t('connections.form.scriptModeAddingConnections', { count: fullyProcessedConnections.length })); + + let successCount = 0; + let errorCount = 0; + let firstErrorEncountered: string | null = null; + + for (const finalConnectionData of fullyProcessedConnections) { + const success = await connectionsStore.addConnection(finalConnectionData); + if (success) { + successCount++; + } else { + errorCount++; + if (!firstErrorEncountered) { + firstErrorEncountered = connectionsStore.error || t('errors.unknown', '未知错误'); + } + console.error(`Failed to add connection: ${finalConnectionData.name}`, connectionsStore.error); + } + } + + if (errorCount > 0) { + const message = t('connections.form.errorBatchAddResult', { successCount, errorCount, firstErrorEncountered: firstErrorEncountered || t('errors.unknown', '未知错误') }); + if (successCount > 0) { + uiNotificationsStore.showWarning(message); + } else { + uiNotificationsStore.showError(message); + } + } + + if (successCount > 0) { + if (errorCount === 0) { + uiNotificationsStore.showSuccess(t('connections.form.successBatchAddResult', { successCount })); + } + emit('connection-added'); + if (errorCount === 0) { + scriptInputText.value = ''; + } + } + }; + + const handleSubmit = async () => { + if (isScriptModeActive.value) { + await handleScriptModeSubmit(); + return; + } + + formError.value = null; + connectionsStore.error = null; + proxiesStore.error = null; + + const availableTagIds = tags.value.map(t_ => t_.id); + const currentSelectedValidTagIds = formData.tag_ids.filter(id => availableTagIds.includes(id)); + + if (!formData.host || !formData.username) { + uiNotificationsStore.showError(t('connections.form.errorRequiredFields')); + return; + } + if (formData.port <= 0 || formData.port > 65535) { + uiNotificationsStore.showError(t('connections.form.errorPort')); + return; + } + + if (formData.type === 'SSH') { + if (!isEditMode.value) { + if (formData.auth_method === 'password' && !formData.password && !formData.host.includes('~')) { + uiNotificationsStore.showError(t('connections.form.errorPasswordRequired')); + return; + } + if (formData.auth_method === 'key' && !formData.selected_ssh_key_id && !formData.host.includes('~')) { + uiNotificationsStore.showError(t('connections.form.errorSshKeyRequired')); + return; + } + } else { + if (formData.auth_method === 'password' && !formData.password && connectionToEdit.value?.auth_method !== 'password') { + uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredOnSwitch')); + return; + } + if (formData.auth_method === 'key' && !formData.selected_ssh_key_id && connectionToEdit.value?.auth_method !== 'key') { + uiNotificationsStore.showError(t('connections.form.errorSshKeyRequiredOnSwitch')); + return; + } + } + } else if (formData.type === 'RDP') { + if (!isEditMode.value && !formData.password && !formData.host.includes('~')) { + uiNotificationsStore.showError(t('connections.form.errorPasswordRequired')); + return; + } + } else if (formData.type === 'VNC') { + if (!isEditMode.value && !formData.vncPassword && !formData.host.includes('~')) { + uiNotificationsStore.showError(t('connections.form.errorVncPasswordRequired', 'VNC 密码是必填项。')); + return; + } + } + + if (!isEditMode.value && formData.host.includes('~')) { + const parsedIpsResult = parseIpRange(formData.host); + + if (Array.isArray(parsedIpsResult)) { + const ips = parsedIpsResult; + if (formData.type === 'SSH' && formData.auth_method === 'key' && !formData.selected_ssh_key_id) { + uiNotificationsStore.showError(t('connections.form.errorSshKeyRequiredForBatch', '批量添加 SSH (密钥认证) 连接时,必须选择一个 SSH 密钥。')); + return; + } + if (formData.type === 'SSH' && formData.auth_method === 'password' && !formData.password) { + uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchSSH', '批量添加 SSH (密码认证) 连接时,必须提供密码。')); + return; + } + if (formData.type === 'RDP' && !formData.password) { + uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchRDP', '批量添加 RDP 连接时,必须提供密码。')); + return; + } + if (formData.type === 'VNC' && !formData.vncPassword) { + uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchVNC', '批量添加 VNC 连接时,必须提供 VNC 密码。')); + return; + } + + let successCount = 0; + let errorCount = 0; + let firstErrorEncountered: string | null = null; + + for (let i = 0; i < ips.length; i++) { + const currentIp = ips[i]; + const ipSuffix = currentIp.split('.').pop() || `${i + 1}`; + + const dataForThisIp: any = { + type: formData.type, + name: formData.name ? `${formData.name}-${ipSuffix}` : currentIp, + host: currentIp, + port: formData.port, + username: formData.username, + notes: formData.notes, + proxy_id: formData.proxy_id || null, + tag_ids: currentSelectedValidTagIds, + }; + + if (formData.type === 'SSH') { + dataForThisIp.auth_method = formData.auth_method; + if (formData.auth_method === 'password') { + dataForThisIp.password = formData.password; + } else if (formData.auth_method === 'key') { + dataForThisIp.ssh_key_id = formData.selected_ssh_key_id; + } + } else if (formData.type === 'RDP') { + dataForThisIp.password = formData.password; + delete dataForThisIp.auth_method; + } else if (formData.type === 'VNC') { + dataForThisIp.password = formData.vncPassword; + delete dataForThisIp.auth_method; + } + + if (dataForThisIp.type !== 'SSH' || dataForThisIp.auth_method !== 'key') delete dataForThisIp.ssh_key_id; + if (dataForThisIp.type === 'SSH' && dataForThisIp.auth_method === 'key') delete dataForThisIp.password; + if (dataForThisIp.type !== 'SSH') delete dataForThisIp.auth_method; + + const success = await connectionsStore.addConnection(dataForThisIp); + if (success) { + successCount++; + } else { + errorCount++; + if (!firstErrorEncountered) { + firstErrorEncountered = connectionsStore.error || t('errors.unknown', '未知错误'); + } + } + } + + if (errorCount > 0) { + const message = t('connections.form.errorBatchAddResult', { successCount, errorCount, firstErrorEncountered: firstErrorEncountered || t('errors.unknown', '未知错误') }); + if (successCount > 0) { + uiNotificationsStore.showWarning(message); + } else { + uiNotificationsStore.showError(message); + } + } else if (successCount > 0) { + uiNotificationsStore.showSuccess(t('connections.form.successBatchAddResult', { successCount })); + emit('connection-added'); + } + return; + } else if (parsedIpsResult.error && parsedIpsResult.error !== 'not_a_range') { + uiNotificationsStore.showError(parsedIpsResult.error); + return; + } + } + + if (isEditMode.value && formData.host.includes('~')) { + uiNotificationsStore.showError(t('connections.form.errorIpRangeNotAllowedInEditMode', '编辑模式下不支持 IP 范围。请使用单个 IP 地址。')); + return; + } + + const dataToSend: any = { + type: formData.type, + name: formData.name, + host: formData.host, + port: formData.port, + notes: formData.notes, + username: formData.username, + proxy_id: formData.proxy_id || null, + tag_ids: currentSelectedValidTagIds, + }; + + if (formData.type === 'SSH') { + dataToSend.auth_method = formData.auth_method; + if (formData.auth_method === 'password') { + if (formData.password) dataToSend.password = formData.password; + } else if (formData.auth_method === 'key') { + if (formData.selected_ssh_key_id) { + dataToSend.ssh_key_id = formData.selected_ssh_key_id; + } + } + } else if (formData.type === 'RDP') { + if (formData.password) dataToSend.password = formData.password; + delete dataToSend.auth_method; + } else if (formData.type === 'VNC') { + if (formData.vncPassword) dataToSend.password = formData.vncPassword; + delete dataToSend.auth_method; + } + + if (dataToSend.type !== 'SSH' || dataToSend.auth_method !== 'key') delete dataToSend.ssh_key_id; + if (dataToSend.type === 'SSH' && dataToSend.auth_method === 'key') delete dataToSend.password; + if (dataToSend.type !== 'SSH') delete dataToSend.auth_method; + + let success = false; + if (isEditMode.value && connectionToEdit.value) { + success = await connectionsStore.updateConnection(connectionToEdit.value.id, dataToSend); + if (success) { + emit('connection-updated'); + } else { + uiNotificationsStore.showError(t('connections.form.errorUpdate', { error: connectionsStore.error || '未知错误' })); + } + } else { + success = await connectionsStore.addConnection(dataToSend); + if (success) { + emit('connection-added'); + } else { + uiNotificationsStore.showError(t('connections.form.errorAdd', { error: connectionsStore.error || '未知错误' })); + } + } + }; + + // 处理删除连接 + const handleDeleteConnection = async () => { + if (!isEditMode.value || !connectionToEdit.value) return; + + const connectionName = connectionToEdit.value.name || `ID: ${connectionToEdit.value.id}`; + if (!confirm(t('connections.prompts.confirmDelete', { name: connectionName }))) { + return; + } + + formError.value = null; + connectionsStore.error = null; + + const success = await connectionsStore.deleteConnection(connectionToEdit.value.id); + if (success) { + emit('connection-deleted'); + emit('close'); + } else { + uiNotificationsStore.showError(t('connections.form.errorDelete', { error: connectionsStore.error || t('errors.unknown', '未知错误') })); + } + }; + + // --- Tag Creation/Deletion Handling --- + const handleCreateTag = async (tagName: string) => { + if (!tagName || tagName.trim().length === 0) return; + const newTag = await tagsStore.addTag(tagName.trim()); + if (newTag && !formData.tag_ids.includes(newTag.id)) { + formData.tag_ids.push(newTag.id); + } + }; + + const handleDeleteTag = async (tagId: number) => { + const tagToDelete = tags.value.find(t_ => t_.id === tagId); + if (!tagToDelete) return; + + if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) { + const success = await tagsStore.deleteTag(tagId); + if (!success) { + alert(t('tags.errorDelete', { error: tagsStore.error || '未知错误' })); + } + } + }; + + // 处理测试连接 + const handleTestConnection = async () => { + testStatus.value = 'testing'; + testResult.value = null; + testLatency.value = null; + + try { + let response; + if (isEditMode.value && connectionToEdit.value) { + response = await apiClient.post(`/connections/${connectionToEdit.value.id}/test`); + } else { + const dataToSend = { + host: formData.host, + port: formData.port, + username: formData.username, + auth_method: formData.auth_method, + password: formData.auth_method === 'password' ? formData.password : undefined, + proxy_id: formData.proxy_id || null, + ssh_key_id: formData.auth_method === 'key' ? formData.selected_ssh_key_id : undefined, + }; + + if (!dataToSend.host || !dataToSend.port || !dataToSend.username || !dataToSend.auth_method) { + throw new Error(t('connections.test.errorMissingFields')); + } + if (dataToSend.auth_method === 'password' && !formData.password) { + throw new Error(t('connections.form.errorPasswordRequired')); + } + if (dataToSend.auth_method === 'key' && !dataToSend.ssh_key_id) { + throw new Error(t('connections.form.errorSshKeyRequired')); + } + response = await apiClient.post('/connections/test-unsaved', dataToSend); + } + + if (response.data.success) { + testStatus.value = 'success'; + testLatency.value = response.data.latency; + testResult.value = `${response.data.latency} ms`; + } else { + testStatus.value = 'error'; + const errorMessage = response.data.message || t('connections.test.errorUnknown'); + testResult.value = errorMessage; + uiNotificationsStore.showError(errorMessage); + } + + } catch (error: any) { + console.error('测试连接失败:', error); + testStatus.value = 'error'; + let errorMessageToShow: string; + if (error.response && error.response.data && error.response.data.message) { + errorMessageToShow = error.response.data.message; + } else { + errorMessageToShow = error.message || t('connections.test.errorNetwork'); + } + testResult.value = errorMessageToShow; + uiNotificationsStore.showError(errorMessageToShow); + } + }; + + // 计算延迟颜色 + const latencyColor = computed(() => { + if (testStatus.value !== 'success' || testLatency.value === null) { + return 'inherit'; + } + const latency = testLatency.value; + if (latency < 100) return 'var(--color-success, #28a745)'; + if (latency < 500) return 'var(--color-warning, #ffc107)'; + return 'var(--color-danger, #dc3545)'; + }); + + // 计算测试按钮文本 + const testButtonText = computed(() => { + if (testStatus.value === 'testing') { + return t('connections.form.testing'); + } + return t('connections.form.testConnection'); + }); + + return { + formData, + isLoading, + testStatus, + testResult, + testLatency, + isScriptModeActive, + scriptInputText, + isEditMode, + formTitle, + submitButtonText, + proxies, // for