865 lines
34 KiB
TypeScript
865 lines
34 KiB
TypeScript
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, proxyName: 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, proxyName: 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;
|
|
let proxyName: 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, proxyName, 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 '-proxy':
|
|
proxyName = 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, proxyName, 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, proxyName, 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, proxyName, tags, note, error: t('connections.form.scriptErrorInvalidUserHostPort', { part: userHostPortPart })};
|
|
}
|
|
|
|
return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, 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,
|
|
proxy_name: parsed.proxyName,
|
|
};
|
|
|
|
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.proxy_name) {
|
|
const foundProxy = proxies.value.find(p => p.name === connData.proxy_name);
|
|
if (foundProxy) {
|
|
connData.proxy_id = foundProxy.id;
|
|
} else {
|
|
uiNotificationsStore.showError(t('proxies.errors.notFound', { name: connData.proxy_name })); // Assuming you add this translation
|
|
resolutionErrorOccurred = true;
|
|
break;
|
|
}
|
|
delete connData.proxy_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
|
|
};
|
|
} |