@@ -66,9 +66,20 @@ const hostTooltipStyle = ref({});
|
|||||||
const hostIconRef = ref<HTMLElement | null>(null);
|
const hostIconRef = ref<HTMLElement | null>(null);
|
||||||
const hostTooltipContentRef = ref<HTMLElement | null>(null);
|
const hostTooltipContentRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// Script Mode State
|
||||||
|
const isScriptModeActive = ref(false);
|
||||||
|
const scriptInputText = ref('');
|
||||||
|
|
||||||
// 计算属性判断是否为编辑模式
|
// 计算属性判断是否为编辑模式
|
||||||
const isEditMode = computed(() => !!props.connectionToEdit);
|
const isEditMode = computed(() => !!props.connectionToEdit);
|
||||||
|
|
||||||
|
// When switching to edit mode, disable script mode
|
||||||
|
watch(isEditMode, (editing) => {
|
||||||
|
if (editing) {
|
||||||
|
isScriptModeActive.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 计算属性动态设置表单标题
|
// 计算属性动态设置表单标题
|
||||||
const formTitle = computed(() => {
|
const formTitle = computed(() => {
|
||||||
return isEditMode.value ? t('connections.form.titleEdit') : t('connections.form.title');
|
return isEditMode.value ? t('connections.form.titleEdit') : t('connections.form.title');
|
||||||
@@ -204,8 +215,338 @@ const parseIpRange = (ipRangeStr: string): string[] | { error: string } => {
|
|||||||
return ips;
|
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 } => {
|
||||||
|
// 首先提取 user@host:port 部分
|
||||||
|
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'; // Default to 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') {
|
||||||
|
// 标签将在后续处理
|
||||||
|
} else if (arg === '-note') {
|
||||||
|
// 备注将在后续处理
|
||||||
|
}
|
||||||
|
|
||||||
|
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(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation for userHostPort
|
||||||
|
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; // Stop on first error for now, or collect all errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.type) { // Should be caught by parsed.error, but as a safeguard
|
||||||
|
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_ids will be resolved later if tags are provided
|
||||||
|
tag_names: parsed.tags, // Store tag names for now, resolve to IDs before API call
|
||||||
|
};
|
||||||
|
|
||||||
|
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 { // key auth
|
||||||
|
if (!parsed.keyName) { // Should not happen if auth_method is 'key'
|
||||||
|
uiNotificationsStore.showError(t('connections.form.scriptErrorMissingKeyNameForSsh', { line }));
|
||||||
|
allConnectionsValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We'll need to find ssh_key_id from keyName later
|
||||||
|
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) {
|
||||||
|
// This case should ideally not be reached if parsing is correct
|
||||||
|
uiNotificationsStore.showError(t('connections.form.scriptErrorInternal', '内部解析错误。'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Parsed connections to add (pre-resolution):', connectionsToAdd);
|
||||||
|
|
||||||
|
const fullyProcessedConnections = [];
|
||||||
|
let resolutionErrorOccurred = false;
|
||||||
|
|
||||||
|
// Resolve tag names and SSH key names to IDs
|
||||||
|
for (const connData of connectionsToAdd) {
|
||||||
|
// Resolve Tag IDs
|
||||||
|
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);
|
||||||
|
if (foundTag) {
|
||||||
|
tagIds.push(foundTag.id);
|
||||||
|
} else {
|
||||||
|
// Option: Create tag if not found, or show error. For now, error.
|
||||||
|
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; // Remove temporary field
|
||||||
|
|
||||||
|
// Resolve SSH Key ID
|
||||||
|
if (connData.type === 'SSH' && connData.auth_method === 'key' && connData.ssh_key_name) {
|
||||||
|
const foundKey = sshKeysStore.sshKeys.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; // Remove temporary field
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up unnecessary fields based on type and auth_method, similar to single add
|
||||||
|
if (connData.type !== 'SSH' || connData.auth_method !== 'key') delete connData.ssh_key_id;
|
||||||
|
if (connData.type === 'SSH' && connData.auth_method === 'key') delete connData.password; // No password if key auth
|
||||||
|
if (connData.type !== 'SSH') delete connData.auth_method; // RDP/VNC don't have auth_method in backend
|
||||||
|
|
||||||
|
fullyProcessedConnections.push(connData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolutionErrorOccurred || (fullyProcessedConnections.length === 0 && lines.length > 0)) {
|
||||||
|
// Errors shown by resolver, or if no connections were processed but there were lines
|
||||||
|
if (!resolutionErrorOccurred && lines.length > 0 && fullyProcessedConnections.length === 0) {
|
||||||
|
uiNotificationsStore.showError(t('connections.form.scriptErrorNothingToProcess', '没有可处理的有效连接数据。'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullyProcessedConnections.length === 0) { // Should be caught by earlier checks
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fully processed connections:', fullyProcessedConnections);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
let firstErrorEncountered: string | null = null;
|
||||||
|
|
||||||
|
uiNotificationsStore.showInfo(t('connections.form.scriptModeAddingConnections', { count: fullyProcessedConnections.length }));
|
||||||
|
|
||||||
|
for (const finalConnectionData of fullyProcessedConnections) {
|
||||||
|
const success = await connectionsStore.addConnection(finalConnectionData);
|
||||||
|
if (success) {
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
errorCount++;
|
||||||
|
if (!firstErrorEncountered) {
|
||||||
|
firstErrorEncountered = connectionsStore.error || t('errors.unknown', '未知错误');
|
||||||
|
}
|
||||||
|
// Optionally, log which connection failed
|
||||||
|
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) { // All successful
|
||||||
|
uiNotificationsStore.showSuccess(t('connections.form.successBatchAddResult', { successCount }));
|
||||||
|
}
|
||||||
|
emit('connection-added'); // Emit even if there were partial successes
|
||||||
|
if (errorCount === 0) { // Clear input only if all were successful
|
||||||
|
scriptInputText.value = '';
|
||||||
|
// emit('close'); // Optionally close form on full success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If successCount is 0 and errorCount is 0 but there were lines, it means something went wrong before this loop.
|
||||||
|
// That case should be handled by the `fullyProcessedConnections.length === 0` check earlier.
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (isScriptModeActive.value) {
|
||||||
|
await handleScriptModeSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
formError.value = null;
|
formError.value = null;
|
||||||
connectionsStore.error = null;
|
connectionsStore.error = null;
|
||||||
proxiesStore.error = null; // 同时清除代理 store 的错误
|
proxiesStore.error = null; // 同时清除代理 store 的错误
|
||||||
@@ -616,10 +957,12 @@ const handleHostIconMouseLeave = () => {
|
|||||||
<h3 class="text-xl font-semibold text-center mb-6 flex-shrink-0">{{ formTitle }}</h3> <!-- Title -->
|
<h3 class="text-xl font-semibold text-center mb-6 flex-shrink-0">{{ formTitle }}</h3> <!-- Title -->
|
||||||
<form @submit.prevent="handleSubmit" class="flex-grow overflow-y-auto pr-2 space-y-6"> <!-- Form with scroll and spacing -->
|
<form @submit.prevent="handleSubmit" class="flex-grow overflow-y-auto pr-2 space-y-6"> <!-- Form with scroll and spacing -->
|
||||||
|
|
||||||
<!-- Basic Info Section -->
|
<!-- Regular Form Sections (conditionally rendered) -->
|
||||||
<div class="space-y-4 p-4 border border-border rounded-md bg-header/30">
|
<template v-if="!isScriptModeActive">
|
||||||
<h4 class="text-base font-semibold mb-3 pb-2 border-b border-border/50">{{ t('connections.form.sectionBasic', '基本信息') }}</h4>
|
<!-- Basic Info Section -->
|
||||||
<div>
|
<div class="space-y-4 p-4 border border-border rounded-md bg-header/30">
|
||||||
|
<h4 class="text-base font-semibold mb-3 pb-2 border-b border-border/50">{{ t('connections.form.sectionBasic', '基本信息') }}</h4>
|
||||||
|
<div>
|
||||||
<label for="conn-name" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.name') }} ({{ t('connections.form.optional') }})</label>
|
<label for="conn-name" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.name') }} ({{ t('connections.form.optional') }})</label>
|
||||||
<input type="text" id="conn-name" v-model="formData.name"
|
<input type="text" id="conn-name" v-model="formData.name"
|
||||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||||
@@ -656,7 +999,7 @@ const handleHostIconMouseLeave = () => {
|
|||||||
<label for="conn-host" class="block text-sm font-medium text-text-secondary mb-1">
|
<label for="conn-host" class="block text-sm font-medium text-text-secondary mb-1">
|
||||||
{{ t('connections.form.host') }}
|
{{ t('connections.form.host') }}
|
||||||
<span class="relative ml-1" @mouseenter="handleHostIconMouseEnter" @mouseleave="handleHostIconMouseLeave">
|
<span class="relative ml-1" @mouseenter="handleHostIconMouseEnter" @mouseleave="handleHostIconMouseLeave">
|
||||||
<i ref="hostIconRef" class="fas fa-info-circle text-text-secondary cursor-help"></i>
|
<i ref="hostIconRef" class="fas fa-exclamation-circle text-text-secondary cursor-help"></i>
|
||||||
<!-- Tooltip is now handled by Teleport -->
|
<!-- Tooltip is now handled by Teleport -->
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -789,15 +1132,55 @@ const handleHostIconMouseLeave = () => {
|
|||||||
:placeholder="t('connections.form.notesPlaceholder', '输入连接备注...')"></textarea>
|
:placeholder="t('connections.form.notesPlaceholder', '输入连接备注...')"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template> <!-- End of v-if="!isScriptModeActive" -->
|
||||||
|
|
||||||
<!-- Error message DIV removed -->
|
<!-- Script Mode Section Toggle -->
|
||||||
|
<div v-if="!isEditMode" class="space-y-4 p-4 border border-border rounded-md bg-header/30 mt-6">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h4 class="text-base font-semibold">{{ t('connections.form.sectionScriptMode', '脚本模式') }}</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="isScriptModeActive = !isScriptModeActive"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary',
|
||||||
|
isScriptModeActive ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
|
||||||
|
]"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="isScriptModeActive"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||||
|
isScriptModeActive ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isScriptModeActive" class="mt-4">
|
||||||
|
<label for="conn-script-input" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.scriptModeInputLabel', '连接脚本 (每行一个)') }}</label>
|
||||||
|
<textarea
|
||||||
|
id="conn-script-input"
|
||||||
|
v-model="scriptInputText"
|
||||||
|
rows="10"
|
||||||
|
wrap="off"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
|
||||||
|
:placeholder="t('connections.form.scriptModePlaceholder')"
|
||||||
|
></textarea>
|
||||||
|
<p class="mt-1 text-xs text-text-secondary">
|
||||||
|
{{ t('connections.form.scriptModeFormatInfo', '格式: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error message DIV removed -->
|
||||||
|
|
||||||
</form> <!-- End Form -->
|
</form> <!-- End Form -->
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Form Actions -->
|
||||||
<div class="flex justify-between items-center pt-5 mt-6 flex-shrink-0">
|
<div class="flex justify-between items-center pt-5 mt-6 flex-shrink-0">
|
||||||
<!-- Test Area (Only show for SSH) -->
|
<!-- Test Area (Only show for SSH and when script mode is NOT active) -->
|
||||||
<div v-if="formData.type === 'SSH'" class="flex flex-col items-start gap-1">
|
<div v-if="formData.type === 'SSH' && !isScriptModeActive" class="flex flex-col items-start gap-1">
|
||||||
<div class="flex items-center gap-2"> <!-- Button and Icon -->
|
<div class="flex items-center gap-2"> <!-- Button and Icon -->
|
||||||
<button type="button" @click="handleTestConnection" :disabled="isLoading || testStatus === 'testing'"
|
<button type="button" @click="handleTestConnection" :disabled="isLoading || testStatus === 'testing'"
|
||||||
class="px-3 py-1.5 border border-border rounded-md text-sm font-medium text-text-secondary bg-background hover:bg-border focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center transition-colors duration-150">
|
class="px-3 py-1.5 border border-border rounded-md text-sm font-medium text-text-secondary bg-background hover:bg-border focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center transition-colors duration-150">
|
||||||
@@ -829,10 +1212,11 @@ const handleHostIconMouseLeave = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Placeholder for alignment when test button is hidden -->
|
<!-- Placeholder for alignment when test button is hidden or script mode is active -->
|
||||||
<div v-else class="flex-1"></div> <!-- This div ensures the main action buttons are pushed to the right when test area is hidden -->
|
<div v-else-if="!isScriptModeActive" class="flex-1"></div>
|
||||||
|
<div v-else class="flex-1"></div> <!-- Also take up space if script mode is active, pushing buttons right -->
|
||||||
<div class="flex space-x-3"> <!-- Main Actions -->
|
<div class="flex space-x-3"> <!-- Main Actions -->
|
||||||
<button v-if="isEditMode" type="button" @click="handleDeleteConnection" :disabled="isLoading || (formData.type === 'SSH' && testStatus === 'testing')"
|
<button v-if="isEditMode && !isScriptModeActive" type="button" @click="handleDeleteConnection" :disabled="isLoading || (formData.type === 'SSH' && testStatus === 'testing')"
|
||||||
class="px-4 py-2 bg-transparent text-red-600 border border-red-500 rounded-md shadow-sm hover:bg-red-500/10 hover:text-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
|
class="px-4 py-2 bg-transparent text-red-600 border border-red-500 rounded-md shadow-sm hover:bg-red-500/10 hover:text-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
|
||||||
{{ t('connections.actions.delete') }}
|
{{ t('connections.actions.delete') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -196,7 +196,31 @@
|
|||||||
"errorPasswordRequiredForBatchVNC": "When batch adding VNC connections, a VNC password must be provided.",
|
"errorPasswordRequiredForBatchVNC": "When batch adding VNC connections, a VNC password must be provided.",
|
||||||
"errorBatchAddResult": "Batch add: {successCount} succeeded, {errorCount} failed. First error: {firstErrorEncountered}",
|
"errorBatchAddResult": "Batch add: {successCount} succeeded, {errorCount} failed. First error: {firstErrorEncountered}",
|
||||||
"successBatchAddResult": "Batch add successful: {successCount} connections created.",
|
"successBatchAddResult": "Batch add successful: {successCount} connections created.",
|
||||||
"errorIpRangeNotAllowedInEditMode": "IP range is not supported in edit mode. Please use a single IP address."
|
"errorIpRangeNotAllowedInEditMode": "IP range is not supported in edit mode. Please use a single IP address.",
|
||||||
|
"scriptModeSubmitPlaceholder": "Script mode submission logic to be implemented.",
|
||||||
|
"scriptModeEmpty": "Script input cannot be empty.",
|
||||||
|
"scriptModeSubmitPending": "Processing script mode submission...",
|
||||||
|
"sectionScriptMode": "Script Mode",
|
||||||
|
"scriptModeInputLabel": "Connection Script (one per line)",
|
||||||
|
"scriptModePlaceholder": "Enter connection script, one connection configuration per line.",
|
||||||
|
"scriptModeFormatInfo": "Format: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]",
|
||||||
|
"scriptErrorMissingHost": "Script line '{line}' is missing the 'user@host:port' part.",
|
||||||
|
"scriptErrorInvalidType": "Invalid type '{type}' in script line '{line}'. Valid types are SSH, RDP, VNC.",
|
||||||
|
"scriptErrorUnknownArg": "Unknown argument '{arg}' in script line '{line}'.",
|
||||||
|
"scriptErrorUnexpectedToken": "Unexpected token '{token}' in script line '{line}'.",
|
||||||
|
"scriptErrorInvalidUserHostPort": "Invalid format for '{part}' in script line '{line}'. Expected 'user@host' or 'user@host:port'.",
|
||||||
|
"scriptErrorInLine": "Error parsing script line: \"{line}\" - Error: {error}",
|
||||||
|
"scriptErrorMissingType": "Script line '{line}' is missing connection type or type is invalid.",
|
||||||
|
"scriptErrorInvalidUserHostFormat": "Invalid user@host format in script line '{line}'.",
|
||||||
|
"scriptErrorInvalidPort": "Invalid port '{port}' in script line '{line}'.",
|
||||||
|
"scriptErrorMissingPasswordForSsh": "Script line '{line}' (SSH password auth) is missing password (-p).",
|
||||||
|
"scriptErrorMissingKeyNameForSsh": "Script line '{line}' (SSH key auth) is missing key name (-k).",
|
||||||
|
"scriptErrorMissingPasswordForType": "Script line '{line}' (type {type}) is missing password (-p).",
|
||||||
|
"scriptErrorInternal": "Internal parsing error while processing script input.",
|
||||||
|
"scriptErrorTagNotFound": "Script processing error: Tag '{tagName}' not found.",
|
||||||
|
"scriptErrorSshKeyNotFound": "Script processing error: SSH Key '{keyName}' not found.",
|
||||||
|
"scriptErrorNothingToProcess": "No valid connection data to process.",
|
||||||
|
"scriptModeAddingConnections": "Adding {count} connections via script mode..."
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"success": "Connection test successful!",
|
"success": "Connection test successful!",
|
||||||
|
|||||||
@@ -181,7 +181,31 @@
|
|||||||
"errorPasswordRequiredForBatchVNC": "VNC接続を一括追加する場合、VNCパスワードを提供する必要があります。",
|
"errorPasswordRequiredForBatchVNC": "VNC接続を一括追加する場合、VNCパスワードを提供する必要があります。",
|
||||||
"errorBatchAddResult": "一括追加: {successCount} 件成功, {errorCount} 件失敗。最初のエラー: {firstErrorEncountered}",
|
"errorBatchAddResult": "一括追加: {successCount} 件成功, {errorCount} 件失敗。最初のエラー: {firstErrorEncountered}",
|
||||||
"successBatchAddResult": "一括追加成功: {successCount} 件の接続が作成されました。",
|
"successBatchAddResult": "一括追加成功: {successCount} 件の接続が作成されました。",
|
||||||
"errorIpRangeNotAllowedInEditMode": "編集モードではIP範囲はサポートされていません。単一のIPアドレスを使用してください。"
|
"errorIpRangeNotAllowedInEditMode": "編集モードではIP範囲はサポートされていません。単一のIPアドレスを使用してください。",
|
||||||
|
"scriptModeSubmitPlaceholder": "スクリプトモードの送信ロジックは実装予定です。",
|
||||||
|
"scriptModeEmpty": "スクリプト入力は空にできません。",
|
||||||
|
"scriptModeSubmitPending": "スクリプトモードの送信を処理中...",
|
||||||
|
"sectionScriptMode": "スクリプトモード",
|
||||||
|
"scriptModeInputLabel": "接続スクリプト (1行に1つ)",
|
||||||
|
"scriptModePlaceholder": "接続スクリプトを入力してください。1行に1つの接続設定。",
|
||||||
|
"scriptModeFormatInfo": "形式: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]",
|
||||||
|
"scriptErrorMissingHost": "スクリプト行 '{line}' には 'user@host:port' 部分がありません。",
|
||||||
|
"scriptErrorInvalidType": "スクリプト行 '{line}' のタイプ '{type}' は無効です。有効なタイプは SSH, RDP, VNC です。",
|
||||||
|
"scriptErrorUnknownArg": "スクリプト行 '{line}' に不明な引数 '{arg}' があります。",
|
||||||
|
"scriptErrorUnexpectedToken": "スクリプト行 '{line}' に予期しないトークン '{token}' があります。",
|
||||||
|
"scriptErrorInvalidUserHostPort": "スクリプト行 '{line}' の '{part}' 部分の形式が無効です。期待される形式は 'user@host' または 'user@host:port' です。",
|
||||||
|
"scriptErrorInLine": "スクリプト行の解析中にエラーが発生しました: \"{line}\" - エラー: {error}",
|
||||||
|
"scriptErrorMissingType": "スクリプト行 '{line}' には接続タイプがないか、タイプが無効です。",
|
||||||
|
"scriptErrorInvalidUserHostFormat": "スクリプト行 '{line}' の user@host 部分の形式が無効です。",
|
||||||
|
"scriptErrorInvalidPort": "スクリプト行 '{line}' のポート '{port}' は無効です。",
|
||||||
|
"scriptErrorMissingPasswordForSsh": "スクリプト行 '{line}' (SSHパスワード認証) にはパスワード (-p) がありません。",
|
||||||
|
"scriptErrorMissingKeyNameForSsh": "スクリプト行 '{line}' (SSHキー認証) にはキー名 (-k) がありません。",
|
||||||
|
"scriptErrorMissingPasswordForType": "スクリプト行 '{line}' ({type}タイプ) にはパスワード (-p) がありません。",
|
||||||
|
"scriptErrorInternal": "スクリプト入力の処理中に内部解析エラーが発生しました。",
|
||||||
|
"scriptErrorTagNotFound": "スクリプト処理エラー: タグ '{tagName}' が見つかりません。",
|
||||||
|
"scriptErrorSshKeyNotFound": "スクリプト処理エラー: SSH キー '{keyName}' が見つかりません。",
|
||||||
|
"scriptErrorNothingToProcess": "処理する有効な接続データがありません。",
|
||||||
|
"scriptModeAddingConnections": "スクリプトモードで {count} 個の接続を追加しています..."
|
||||||
},
|
},
|
||||||
"noConnections": "接続がありません。'新しい接続を追加'をクリックして作成してください。",
|
"noConnections": "接続がありません。'新しい接続を追加'をクリックして作成してください。",
|
||||||
"noUntaggedConnections": "タグなしの接続はありません。",
|
"noUntaggedConnections": "タグなしの接続はありません。",
|
||||||
|
|||||||
@@ -195,7 +195,31 @@
|
|||||||
"errorPasswordRequiredForBatchVNC": "批量添加 VNC 连接时,必须提供 VNC 密码。",
|
"errorPasswordRequiredForBatchVNC": "批量添加 VNC 连接时,必须提供 VNC 密码。",
|
||||||
"errorBatchAddResult": "批量添加: {successCount} 个成功, {errorCount} 个失败。首个错误: {firstErrorEncountered}",
|
"errorBatchAddResult": "批量添加: {successCount} 个成功, {errorCount} 个失败。首个错误: {firstErrorEncountered}",
|
||||||
"successBatchAddResult": "批量添加成功: {successCount} 个连接已创建。",
|
"successBatchAddResult": "批量添加成功: {successCount} 个连接已创建。",
|
||||||
"errorIpRangeNotAllowedInEditMode": "编辑模式下不支持 IP 范围。请使用单个 IP 地址。"
|
"errorIpRangeNotAllowedInEditMode": "编辑模式下不支持 IP 范围。请使用单个 IP 地址。",
|
||||||
|
"scriptModeSubmitPlaceholder": "脚本模式提交逻辑待实现。",
|
||||||
|
"scriptModeEmpty": "脚本输入不能为空。",
|
||||||
|
"scriptModeSubmitPending": "正在处理脚本模式提交...",
|
||||||
|
"sectionScriptMode": "脚本模式",
|
||||||
|
"scriptModeInputLabel": "连接脚本 (每行一个)",
|
||||||
|
"scriptModePlaceholder": "请输入连接脚本,每行一个连接配置。",
|
||||||
|
"scriptModeFormatInfo": "格式: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]",
|
||||||
|
"scriptErrorMissingHost": "脚本行 '{line}' 缺少 'user@host:port' 部分。",
|
||||||
|
"scriptErrorInvalidType": "脚本行 '{line}' 中的类型 '{type}' 无效。有效类型为 SSH, RDP, VNC。",
|
||||||
|
"scriptErrorUnknownArg": "脚本行 '{line}' 中存在未知参数 '{arg}'。",
|
||||||
|
"scriptErrorUnexpectedToken": "脚本行 '{line}' 中出现意外标记 '{token}'。",
|
||||||
|
"scriptErrorInvalidUserHostPort": "脚本行 '{line}' 中的 '{part}' 部分格式无效。期望格式为 'user@host' 或 'user@host:port'。",
|
||||||
|
"scriptErrorInLine": "解析脚本行时出错: \"{line}\" - 错误: {error}",
|
||||||
|
"scriptErrorMissingType": "脚本行 '{line}' 缺少连接类型或类型无效。",
|
||||||
|
"scriptErrorInvalidUserHostFormat": "脚本行 '{line}' 的 user@host 部分格式无效。",
|
||||||
|
"scriptErrorInvalidPort": "脚本行 '{line}' 的端口 '{port}' 无效。",
|
||||||
|
"scriptErrorMissingPasswordForSsh": "脚本行 '{line}' (SSH密码认证) 缺少密码 (-p)。",
|
||||||
|
"scriptErrorMissingKeyNameForSsh": "脚本行 '{line}' (SSH密钥认证) 缺少密钥名称 (-k)。",
|
||||||
|
"scriptErrorMissingPasswordForType": "脚本行 '{line}' ({type}类型) 缺少密码 (-p)。",
|
||||||
|
"scriptErrorInternal": "处理脚本输入时发生内部解析错误。",
|
||||||
|
"scriptErrorTagNotFound": "脚本处理错误:未找到标签 '{tagName}'。",
|
||||||
|
"scriptErrorSshKeyNotFound": "脚本处理错误:未找到 SSH 密钥 '{keyName}'。",
|
||||||
|
"scriptErrorNothingToProcess": "没有可处理的有效连接数据。",
|
||||||
|
"scriptModeAddingConnections": "正在通过脚本模式添加 {count} 个连接..."
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"success": "连接测试成功!",
|
"success": "连接测试成功!",
|
||||||
|
|||||||
Reference in New Issue
Block a user