Files
nexus-terminal/packages/frontend/src/components/AddConnectionForm.vue
T
Baobhan Sith 80d5ab46ff update
2025-05-11 18:23:00 +08:00

214 lines
11 KiB
Vue

<script setup lang="ts">
import { ref, Teleport, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { ConnectionInfo } from '../stores/connections.store'; // Keep ConnectionInfo type
import { useAddConnectionForm } from '../composables/useAddConnectionForm';
import AddConnectionFormBasicInfo from './AddConnectionFormBasicInfo.vue';
import AddConnectionFormAuth from './AddConnectionFormAuth.vue';
import AddConnectionFormAdvanced from './AddConnectionFormAdvanced.vue';
// 定义组件发出的事件
const emit = defineEmits(['close', 'connection-added', 'connection-updated', 'connection-deleted']);
// 定义 Props
const props = defineProps<{
connectionToEdit: ConnectionInfo | null;
}>();
const { t } = useI18n();
const {
formData,
isLoading,
testStatus,
testResult,
testLatency,
isScriptModeActive,
scriptInputText,
isEditMode,
formTitle,
submitButtonText,
proxies,
tags,
isProxyLoading,
proxyStoreError,
isTagLoading,
tagStoreError,
handleSubmit,
handleDeleteConnection,
handleTestConnection,
handleCreateTag,
handleDeleteTag,
latencyColor,
testButtonText,
} = useAddConnectionForm(props, emit);
// Tooltip state and refs - Kept in component as it's purely view-related
const showHostTooltip = ref(false);
const hostTooltipStyle = ref({});
const hostIconRef = ref<HTMLElement | null>(null);
const hostTooltipContentRef = ref<HTMLElement | null>(null);
const handleHostIconMouseEnter = async () => {
showHostTooltip.value = true;
await nextTick();
if (hostIconRef.value && hostTooltipContentRef.value) {
const iconRect = hostIconRef.value.getBoundingClientRect();
const tooltipRect = hostTooltipContentRef.value.getBoundingClientRect();
let top = iconRect.top - tooltipRect.height - 8;
let left = iconRect.left + (iconRect.width / 2) - (tooltipRect.width / 2);
if (top < 0) {
top = iconRect.bottom + 8;
}
if (left < 0) {
left = 0;
}
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width;
}
hostTooltipStyle.value = {
top: `${top}px`,
left: `${left}px`,
};
}
};
const handleHostIconMouseLeave = () => {
showHostTooltip.value = false;
};
</script>
<template>
<!-- Host Tooltip is managed by AddConnectionFormBasicInfo for its specific host input,
but if there was a general tooltip at this level, Teleport would be here.
The original Teleport for host tooltip is removed as it's now encapsulated. -->
<div class="fixed inset-0 bg-overlay flex justify-center items-center z-50 p-4"> <!-- Overlay -->
<div class="bg-background text-foreground p-6 rounded-lg shadow-xl border border-border w-full max-w-2xl max-h-[90vh] flex flex-col"> <!-- Form Panel -->
<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 -->
<!-- Regular Form Sections (conditionally rendered) -->
<template v-if="!isScriptModeActive">
<AddConnectionFormBasicInfo :form-data="formData" />
<AddConnectionFormAuth :form-data="formData" :is-edit-mode="isEditMode" />
<AddConnectionFormAdvanced
:form-data="formData"
:proxies="proxies"
:tags="tags"
:is-proxy-loading="isProxyLoading"
:proxy-store-error="proxyStoreError"
:is-tag-loading="isTagLoading"
:tag-store-error="tagStoreError"
@create-tag="handleCreateTag"
@delete-tag="handleDeleteTag"
/>
</template> <!-- End of v-if="!isScriptModeActive" -->
<!-- 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 Actions -->
<div class="flex justify-between items-center pt-5 mt-6 flex-shrink-0">
<!-- Test Area (Only show for SSH and when script mode is NOT active) -->
<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 -->
<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">
<svg v-if="testStatus === 'testing'" class="animate-spin -ml-0.5 mr-2 h-4 w-4 text-text-secondary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ testButtonText }}
</button>
<div class="relative group"> <!-- Tooltip Container -->
<i class="fas fa-info-circle text-text-secondary cursor-help"></i>
<span class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-max max-w-xs p-2 text-xs text-white bg-gray-800 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10 whitespace-pre-wrap">
{{ t('connections.test.latencyTooltip') }}
</span>
</div>
</div>
<!-- Test Result -->
<div class="min-h-[1.2em] pl-1 text-xs">
<div v-if="testStatus === 'testing'" class="text-text-secondary animate-pulse">
{{ t('connections.test.testingInProgress', '测试中...') }}
</div>
<div v-else-if="testStatus === 'success'" class="font-medium" :style="{ color: latencyColor }">
{{ testResult }}
</div>
<div v-else-if="testStatus === 'error'" class="text-error font-medium">
<!-- Error message is now shown via uiNotificationsStore -->
<!-- Display a generic message or icon here if needed, or leave empty -->
{{ t('connections.test.errorPrefix', '错误:') }} {{ testResult }} <!-- Or simply 'Error' -->
</div>
</div>
</div>
<!-- Placeholder for alignment when test button is hidden or script mode is active -->
<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 -->
<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">
{{ t('connections.actions.delete') }}
</button>
<button type="submit" @click="handleSubmit" :disabled="isLoading || (formData.type === 'SSH' && testStatus === 'testing')"
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
{{ submitButtonText }}
</button>
<button type="button" @click="emit('close')" :disabled="isLoading || (formData.type === 'SSH' && testStatus === 'testing')"
class="px-4 py-2 bg-transparent text-text-secondary border border-border rounded-md shadow-sm hover:bg-border hover:text-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
{{ t('connections.form.cancel') }}
</button>
</div>
</div> <!-- End Form Actions -->
</div> <!-- End Form Panel -->
</div> <!-- End Overlay -->
</template>
<!-- Scoped styles removed, now using Tailwind utility classes -->