This commit is contained in:
Baobhan Sith
2025-05-11 18:23:00 +08:00
parent 4c634c6fde
commit 80d5ab46ff
5 changed files with 1192 additions and 1079 deletions
@@ -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<string | null>(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<string | number | null>(null); // 存储延迟或错误信息
const testLatency = ref<number | null>(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 <select>
tags, // for <TagInput :available-tags="tags">
isProxyLoading,
proxyStoreError,
isTagLoading,
tagStoreError,
handleSubmit,
handleDeleteConnection,
handleTestConnection,
handleCreateTag,
handleDeleteTag,
latencyColor,
testButtonText,
// Expose stores if child components or template parts need direct access, though usually not.
// uiNotificationsStore, // Used internally, not needed to be returned
};
}