feat(frontend): 支持批量编辑和管理已保存登录凭证
为连接表单补充已保存登录凭证的校验与提交流程, 允许新增、批量新增、测试连接时优先使用凭证 在连接列表中新增登录凭证管理入口,并支持批量编辑 时按连接类型筛选和应用已保存凭证 补充中英日文案,并修复 SSH 密钥选择器的绑定兼容性
This commit is contained in:
@@ -7,6 +7,7 @@ import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
|||||||
import { useProxiesStore } from '../stores/proxies.store';
|
import { useProxiesStore } from '../stores/proxies.store';
|
||||||
import { useTagsStore, type TagInfo } from '../stores/tags.store';
|
import { useTagsStore, type TagInfo } from '../stores/tags.store';
|
||||||
import { useSshKeysStore, type SshKeyBasicInfo } from '../stores/sshKeys.store';
|
import { useSshKeysStore, type SshKeyBasicInfo } from '../stores/sshKeys.store';
|
||||||
|
import { useLoginCredentialsStore, type LoginCredentialBasicInfo } from '../stores/loginCredentials.store';
|
||||||
import TagInput from './TagInput.vue';
|
import TagInput from './TagInput.vue';
|
||||||
|
|
||||||
interface BatchUpdateData {
|
interface BatchUpdateData {
|
||||||
@@ -14,6 +15,7 @@ interface BatchUpdateData {
|
|||||||
username?: string | null;
|
username?: string | null;
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
ssh_key_id?: number | null;
|
ssh_key_id?: number | null;
|
||||||
|
login_credential_id?: number | null;
|
||||||
proxy_id?: number | null;
|
proxy_id?: number | null;
|
||||||
tag_ids?: number[];
|
tag_ids?: number[];
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
@@ -38,6 +40,7 @@ const uiNotificationsStore = useUiNotificationsStore();
|
|||||||
const proxiesStore = useProxiesStore();
|
const proxiesStore = useProxiesStore();
|
||||||
const tagsStore = useTagsStore();
|
const tagsStore = useTagsStore();
|
||||||
const sshKeysStore = useSshKeysStore();
|
const sshKeysStore = useSshKeysStore();
|
||||||
|
const loginCredentialsStore = useLoginCredentialsStore();
|
||||||
|
|
||||||
const internalVisible = ref(props.visible);
|
const internalVisible = ref(props.visible);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
@@ -51,6 +54,14 @@ const enableAdvancedEdit = ref(false);
|
|||||||
const availableTags = computed(() => tagsStore.tags as TagInfo[]);
|
const availableTags = computed(() => tagsStore.tags as TagInfo[]);
|
||||||
const availableProxies = computed(() => proxiesStore.proxies);
|
const availableProxies = computed(() => proxiesStore.proxies);
|
||||||
const availableSshKeys = computed(() => sshKeysStore.sshKeys as SshKeyBasicInfo[]);
|
const availableSshKeys = computed(() => sshKeysStore.sshKeys as SshKeyBasicInfo[]);
|
||||||
|
const selectedConnections = computed(() => connectionsStore.connections.filter((conn) => props.connectionIds.includes(conn.id)));
|
||||||
|
const selectedConnectionTypes = computed(() => Array.from(new Set(selectedConnections.value.map((conn) => conn.type))));
|
||||||
|
const availableLoginCredentials = computed(() => {
|
||||||
|
if (selectedConnectionTypes.value.length !== 1) {
|
||||||
|
return [] as LoginCredentialBasicInfo[];
|
||||||
|
}
|
||||||
|
return loginCredentialsStore.loginCredentials.filter((credential) => credential.type === selectedConnectionTypes.value[0]);
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
internalVisible.value = newVal;
|
internalVisible.value = newVal;
|
||||||
@@ -60,6 +71,7 @@ watch(() => props.visible, (newVal) => {
|
|||||||
username: undefined,
|
username: undefined,
|
||||||
password: undefined,
|
password: undefined,
|
||||||
ssh_key_id: undefined,
|
ssh_key_id: undefined,
|
||||||
|
login_credential_id: undefined,
|
||||||
proxy_id: undefined,
|
proxy_id: undefined,
|
||||||
tag_ids: undefined,
|
tag_ids: undefined,
|
||||||
notes: undefined, // Keep notes initialization
|
notes: undefined, // Keep notes initialization
|
||||||
@@ -78,6 +90,9 @@ watch(() => props.visible, (newVal) => {
|
|||||||
if (availableSshKeys.value.length === 0 && !sshKeysStore.isLoading) {
|
if (availableSshKeys.value.length === 0 && !sshKeysStore.isLoading) {
|
||||||
sshKeysStore.fetchSshKeys();
|
sshKeysStore.fetchSshKeys();
|
||||||
}
|
}
|
||||||
|
if (loginCredentialsStore.loginCredentials.length === 0 && !loginCredentialsStore.isLoading) {
|
||||||
|
loginCredentialsStore.fetchLoginCredentials();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,6 +133,14 @@ const handleSave = async () => {
|
|||||||
if (formData.value.ssh_key_id !== undefined) {
|
if (formData.value.ssh_key_id !== undefined) {
|
||||||
updatesToApply.ssh_key_id = formData.value.ssh_key_id;
|
updatesToApply.ssh_key_id = formData.value.ssh_key_id;
|
||||||
}
|
}
|
||||||
|
if (formData.value.login_credential_id !== undefined) {
|
||||||
|
if (selectedConnectionTypes.value.length !== 1 && formData.value.login_credential_id !== null) {
|
||||||
|
uiNotificationsStore.addNotification({ message: t('connections.batchEdit.savedCredentialMixedType', '批量应用已保存凭证前,请先只选择同一种连接类型。'), type: 'warning' });
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatesToApply.login_credential_id = formData.value.login_credential_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableAdvancedEdit.value) {
|
if (enableAdvancedEdit.value) {
|
||||||
@@ -199,6 +222,9 @@ onMounted(() => {
|
|||||||
if (availableSshKeys.value.length === 0 && !sshKeysStore.isLoading) {
|
if (availableSshKeys.value.length === 0 && !sshKeysStore.isLoading) {
|
||||||
sshKeysStore.fetchSshKeys();
|
sshKeysStore.fetchSshKeys();
|
||||||
}
|
}
|
||||||
|
if (loginCredentialsStore.loginCredentials.length === 0 && !loginCredentialsStore.isLoading) {
|
||||||
|
loginCredentialsStore.fetchLoginCredentials();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -264,6 +290,22 @@ onMounted(() => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="batch-login-credential" class="block text-sm font-medium text-text-secondary">{{ t('connections.form.savedLoginCredential', '已保存凭证') }}</label>
|
||||||
|
<select
|
||||||
|
id="batch-login-credential"
|
||||||
|
v-model="formData.login_credential_id"
|
||||||
|
class="mt-1 block 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 sm:text-sm"
|
||||||
|
:disabled="loginCredentialsStore.isLoading"
|
||||||
|
>
|
||||||
|
<option :value="undefined">{{ t('connections.batchEdit.noChange', '-- 不更改 --') }}</option>
|
||||||
|
<option :value="null">{{ t('connections.form.clearSavedCredential', '取消已保存凭证') }}</option>
|
||||||
|
<option v-if="selectedConnectionTypes.length !== 1" disabled>{{ t('connections.batchEdit.savedCredentialTypeHint', '仅支持同类型连接批量应用') }}</option>
|
||||||
|
<option v-for="credential in availableLoginCredentials" :key="credential.id" :value="credential.id">
|
||||||
|
{{ credential.name }} ({{ credential.username }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,4 +375,4 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -304,7 +304,10 @@ const cancelForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.sshKey', 'SSH 密钥') }}</label>
|
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.sshKey', 'SSH 密钥') }}</label>
|
||||||
<SshKeySelector v-model="formData.ssh_key_id" />
|
<SshKeySelector
|
||||||
|
:model-value="formData.ssh_key_id ?? null"
|
||||||
|
@update:model-value="value => formData.ssh_key_id = value"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -612,7 +612,13 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
const availableTagIds = tags.value.map(t_ => t_.id);
|
const availableTagIds = tags.value.map(t_ => t_.id);
|
||||||
const currentSelectedValidTagIds = formData.tag_ids.filter(id => availableTagIds.includes(id));
|
const currentSelectedValidTagIds = formData.tag_ids.filter(id => availableTagIds.includes(id));
|
||||||
|
|
||||||
if (!formData.host || !formData.username) {
|
const usingSavedCredential = formData.credential_source === 'saved';
|
||||||
|
|
||||||
|
if (!formData.host) {
|
||||||
|
uiNotificationsStore.showError(t('connections.form.errorRequiredFields'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!usingSavedCredential && !formData.username) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorRequiredFields'));
|
uiNotificationsStore.showError(t('connections.form.errorRequiredFields'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -621,7 +627,12 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.type === 'SSH') {
|
if (usingSavedCredential && !formData.login_credential_id) {
|
||||||
|
uiNotificationsStore.showError(t('connections.form.errorLoginCredentialRequired', '请选择已保存的登录凭证。'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usingSavedCredential && formData.type === 'SSH') {
|
||||||
if (!isEditMode.value) {
|
if (!isEditMode.value) {
|
||||||
if (formData.auth_method === 'password' && !formData.password && !formData.host.includes('~')) {
|
if (formData.auth_method === 'password' && !formData.password && !formData.host.includes('~')) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorPasswordRequired'));
|
uiNotificationsStore.showError(t('connections.form.errorPasswordRequired'));
|
||||||
@@ -641,12 +652,12 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (formData.type === 'RDP') {
|
} else if (!usingSavedCredential && formData.type === 'RDP') {
|
||||||
if (!isEditMode.value && !formData.password && !formData.host.includes('~')) {
|
if (!isEditMode.value && !formData.password && !formData.host.includes('~')) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorPasswordRequired'));
|
uiNotificationsStore.showError(t('connections.form.errorPasswordRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (formData.type === 'VNC') {
|
} else if (!usingSavedCredential && formData.type === 'VNC') {
|
||||||
if (!isEditMode.value && !formData.vncPassword && !formData.host.includes('~')) {
|
if (!isEditMode.value && !formData.vncPassword && !formData.host.includes('~')) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorVncPasswordRequired', 'VNC 密码是必填项。'));
|
uiNotificationsStore.showError(t('connections.form.errorVncPasswordRequired', 'VNC 密码是必填项。'));
|
||||||
return;
|
return;
|
||||||
@@ -658,19 +669,23 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
|
|
||||||
if (Array.isArray(parsedIpsResult)) {
|
if (Array.isArray(parsedIpsResult)) {
|
||||||
const ips = parsedIpsResult;
|
const ips = parsedIpsResult;
|
||||||
if (formData.type === 'SSH' && formData.auth_method === 'key' && !formData.selected_ssh_key_id) {
|
if (usingSavedCredential && !formData.login_credential_id) {
|
||||||
|
uiNotificationsStore.showError(t('connections.form.errorLoginCredentialRequired', '请选择已保存的登录凭证。'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!usingSavedCredential && formData.type === 'SSH' && formData.auth_method === 'key' && !formData.selected_ssh_key_id) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorSshKeyRequiredForBatch', '批量添加 SSH (密钥认证) 连接时,必须选择一个 SSH 密钥。'));
|
uiNotificationsStore.showError(t('connections.form.errorSshKeyRequiredForBatch', '批量添加 SSH (密钥认证) 连接时,必须选择一个 SSH 密钥。'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formData.type === 'SSH' && formData.auth_method === 'password' && !formData.password) {
|
if (!usingSavedCredential && formData.type === 'SSH' && formData.auth_method === 'password' && !formData.password) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchSSH', '批量添加 SSH (密码认证) 连接时,必须提供密码。'));
|
uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchSSH', '批量添加 SSH (密码认证) 连接时,必须提供密码。'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formData.type === 'RDP' && !formData.password) {
|
if (!usingSavedCredential && formData.type === 'RDP' && !formData.password) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchRDP', '批量添加 RDP 连接时,必须提供密码。'));
|
uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchRDP', '批量添加 RDP 连接时,必须提供密码。'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formData.type === 'VNC' && !formData.vncPassword) {
|
if (!usingSavedCredential && formData.type === 'VNC' && !formData.vncPassword) {
|
||||||
uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchVNC', '批量添加 VNC 连接时,必须提供 VNC 密码。'));
|
uiNotificationsStore.showError(t('connections.form.errorPasswordRequiredForBatchVNC', '批量添加 VNC 连接时,必须提供 VNC 密码。'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -689,23 +704,24 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
host: currentIp,
|
host: currentIp,
|
||||||
port: formData.port,
|
port: formData.port,
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
|
login_credential_id: usingSavedCredential ? formData.login_credential_id : null,
|
||||||
notes: formData.notes,
|
notes: formData.notes,
|
||||||
proxy_id: formData.proxy_id || null,
|
proxy_id: formData.proxy_id || null,
|
||||||
tag_ids: currentSelectedValidTagIds,
|
tag_ids: currentSelectedValidTagIds,
|
||||||
proxy_type: formData.proxy_type,
|
proxy_type: formData.proxy_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (formData.type === 'SSH') {
|
if (!usingSavedCredential && formData.type === 'SSH') {
|
||||||
dataForThisIp.auth_method = formData.auth_method;
|
dataForThisIp.auth_method = formData.auth_method;
|
||||||
if (formData.auth_method === 'password') {
|
if (formData.auth_method === 'password') {
|
||||||
dataForThisIp.password = formData.password;
|
dataForThisIp.password = formData.password;
|
||||||
} else if (formData.auth_method === 'key') {
|
} else if (formData.auth_method === 'key') {
|
||||||
dataForThisIp.ssh_key_id = formData.selected_ssh_key_id;
|
dataForThisIp.ssh_key_id = formData.selected_ssh_key_id;
|
||||||
}
|
}
|
||||||
} else if (formData.type === 'RDP') {
|
} else if (!usingSavedCredential && formData.type === 'RDP') {
|
||||||
dataForThisIp.password = formData.password;
|
dataForThisIp.password = formData.password;
|
||||||
delete dataForThisIp.auth_method;
|
delete dataForThisIp.auth_method;
|
||||||
} else if (formData.type === 'VNC') {
|
} else if (!usingSavedCredential && formData.type === 'VNC') {
|
||||||
dataForThisIp.password = formData.vncPassword;
|
dataForThisIp.password = formData.vncPassword;
|
||||||
delete dataForThisIp.auth_method;
|
delete dataForThisIp.auth_method;
|
||||||
}
|
}
|
||||||
@@ -755,13 +771,14 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
port: formData.port,
|
port: formData.port,
|
||||||
notes: formData.notes,
|
notes: formData.notes,
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
|
login_credential_id: usingSavedCredential ? formData.login_credential_id : null,
|
||||||
proxy_id: formData.proxy_id || null,
|
proxy_id: formData.proxy_id || null,
|
||||||
proxy_type: formData.proxy_type,
|
proxy_type: formData.proxy_type,
|
||||||
tag_ids: currentSelectedValidTagIds,
|
tag_ids: currentSelectedValidTagIds,
|
||||||
jump_chain: formData.jump_chain ? JSON.parse(JSON.stringify(formData.jump_chain)) : null,
|
jump_chain: formData.jump_chain ? JSON.parse(JSON.stringify(formData.jump_chain)) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (formData.type === 'SSH') {
|
if (!usingSavedCredential && formData.type === 'SSH') {
|
||||||
dataToSend.auth_method = formData.auth_method;
|
dataToSend.auth_method = formData.auth_method;
|
||||||
if (formData.auth_method === 'password') {
|
if (formData.auth_method === 'password') {
|
||||||
if (formData.password) dataToSend.password = formData.password;
|
if (formData.password) dataToSend.password = formData.password;
|
||||||
@@ -770,10 +787,10 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
dataToSend.ssh_key_id = formData.selected_ssh_key_id;
|
dataToSend.ssh_key_id = formData.selected_ssh_key_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (formData.type === 'RDP') {
|
} else if (!usingSavedCredential && formData.type === 'RDP') {
|
||||||
if (formData.password) dataToSend.password = formData.password;
|
if (formData.password) dataToSend.password = formData.password;
|
||||||
delete dataToSend.auth_method;
|
delete dataToSend.auth_method;
|
||||||
} else if (formData.type === 'VNC') {
|
} else if (!usingSavedCredential && formData.type === 'VNC') {
|
||||||
if (formData.vncPassword) dataToSend.password = formData.vncPassword;
|
if (formData.vncPassword) dataToSend.password = formData.vncPassword;
|
||||||
delete dataToSend.auth_method;
|
delete dataToSend.auth_method;
|
||||||
}
|
}
|
||||||
@@ -866,16 +883,20 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
auth_method: formData.auth_method,
|
auth_method: formData.auth_method,
|
||||||
password: formData.auth_method === 'password' ? formData.password : undefined,
|
password: formData.auth_method === 'password' ? formData.password : undefined,
|
||||||
proxy_id: formData.proxy_id || null,
|
proxy_id: formData.proxy_id || null,
|
||||||
|
login_credential_id: formData.credential_source === 'saved' ? formData.login_credential_id : null,
|
||||||
ssh_key_id: formData.auth_method === 'key' ? formData.selected_ssh_key_id : undefined,
|
ssh_key_id: formData.auth_method === 'key' ? formData.selected_ssh_key_id : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!dataToSend.host || !dataToSend.port || !dataToSend.username || !dataToSend.auth_method) {
|
if (!dataToSend.host || !dataToSend.port) {
|
||||||
throw new Error(t('connections.test.errorMissingFields'));
|
throw new Error(t('connections.test.errorMissingFields'));
|
||||||
}
|
}
|
||||||
if (dataToSend.auth_method === 'password' && !formData.password) {
|
if (formData.credential_source !== 'saved' && (!dataToSend.username || !dataToSend.auth_method)) {
|
||||||
|
throw new Error(t('connections.test.errorMissingFields'));
|
||||||
|
}
|
||||||
|
if (formData.credential_source !== 'saved' && dataToSend.auth_method === 'password' && !formData.password) {
|
||||||
throw new Error(t('connections.form.errorPasswordRequired'));
|
throw new Error(t('connections.form.errorPasswordRequired'));
|
||||||
}
|
}
|
||||||
if (dataToSend.auth_method === 'key' && !dataToSend.ssh_key_id) {
|
if (formData.credential_source !== 'saved' && dataToSend.auth_method === 'key' && !dataToSend.ssh_key_id) {
|
||||||
throw new Error(t('connections.form.errorSshKeyRequired'));
|
throw new Error(t('connections.form.errorSshKeyRequired'));
|
||||||
}
|
}
|
||||||
response = await apiClient.post('/connections/test-unsaved', dataToSend);
|
response = await apiClient.post('/connections/test-unsaved', dataToSend);
|
||||||
|
|||||||
@@ -215,6 +215,8 @@
|
|||||||
"title": "Batch Edit Connections",
|
"title": "Batch Edit Connections",
|
||||||
"editSelected": "Edit Selected",
|
"editSelected": "Edit Selected",
|
||||||
"noChange":"No change",
|
"noChange":"No change",
|
||||||
|
"savedCredentialMixedType": "Select only one connection type before applying a saved credential in batch.",
|
||||||
|
"savedCredentialTypeHint": "Batch apply works only for the same connection type",
|
||||||
"selectedItems":"{count} items selected",
|
"selectedItems":"{count} items selected",
|
||||||
"deleteSelectedButton": "Delete Selected",
|
"deleteSelectedButton": "Delete Selected",
|
||||||
"deleteSelectedTooltip": "Delete selected connections",
|
"deleteSelectedTooltip": "Delete selected connections",
|
||||||
@@ -273,6 +275,23 @@
|
|||||||
"connectionModeProxy": "Proxy Server",
|
"connectionModeProxy": "Proxy Server",
|
||||||
"connectionModeJumpHost": "Jump Host",
|
"connectionModeJumpHost": "Jump Host",
|
||||||
"connectionType": "Connection Type:",
|
"connectionType": "Connection Type:",
|
||||||
|
"credentialSource": "Credential Source",
|
||||||
|
"credentialSourceDirect": "Account / Password / Key",
|
||||||
|
"credentialSourceSaved": "Use Saved Credential",
|
||||||
|
"savedLoginCredential": "Login Credential",
|
||||||
|
"selectLoginCredential": "Select a saved credential",
|
||||||
|
"manageLoginCredentials": "Manage Login Credentials",
|
||||||
|
"savedCredentialHint": "Saved credentials take priority during test and connect. You can switch back to direct input anytime.",
|
||||||
|
"loginCredentialManager": "Login Credentials",
|
||||||
|
"addLoginCredential": "Add Login Credential",
|
||||||
|
"editLoginCredential": "Edit Login Credential",
|
||||||
|
"noLoginCredentials": "No saved login credentials yet",
|
||||||
|
"clearSavedCredential": "Clear saved credential",
|
||||||
|
"errorLoginCredentialRequired": "Please select a saved login credential.",
|
||||||
|
"errorCredentialRequiredFields": "Name and username are required.",
|
||||||
|
"errorCredentialDetails": "Failed to load login credential details.",
|
||||||
|
"confirmDeleteCredential": "Delete login credential {name}?",
|
||||||
|
"errorDeleteCredential": "Failed to delete login credential.",
|
||||||
"typeSsh": "SSH",
|
"typeSsh": "SSH",
|
||||||
"typeRdp": "RDP",
|
"typeRdp": "RDP",
|
||||||
"typeVnc": "VNC",
|
"typeVnc": "VNC",
|
||||||
|
|||||||
@@ -213,6 +213,23 @@
|
|||||||
"errorPasswordRequiredForBatchSSH": "SSH (パスワード認証) 接続を一括追加する場合、パスワードを提供する必要があります。",
|
"errorPasswordRequiredForBatchSSH": "SSH (パスワード認証) 接続を一括追加する場合、パスワードを提供する必要があります。",
|
||||||
"errorPasswordRequiredForBatchRDP": "RDP接続を一括追加する場合、パスワードを提供する必要があります。",
|
"errorPasswordRequiredForBatchRDP": "RDP接続を一括追加する場合、パスワードを提供する必要があります。",
|
||||||
"errorPasswordRequiredForBatchVNC": "VNC接続を一括追加する場合、VNCパスワードを提供する必要があります。",
|
"errorPasswordRequiredForBatchVNC": "VNC接続を一括追加する場合、VNCパスワードを提供する必要があります。",
|
||||||
|
"credentialSource": "認証ソース",
|
||||||
|
"credentialSourceDirect": "アカウント / パスワード / キー",
|
||||||
|
"credentialSourceSaved": "保存済み認証情報を使用",
|
||||||
|
"savedLoginCredential": "ログイン認証情報",
|
||||||
|
"selectLoginCredential": "ログイン認証情報を選択してください",
|
||||||
|
"manageLoginCredentials": "ログイン認証情報を管理",
|
||||||
|
"savedCredentialHint": "保存済み認証情報はテストと接続時に優先して使用されます。いつでも直接入力へ戻せます。",
|
||||||
|
"loginCredentialManager": "ログイン認証情報",
|
||||||
|
"addLoginCredential": "ログイン認証情報を追加",
|
||||||
|
"editLoginCredential": "ログイン認証情報を編集",
|
||||||
|
"noLoginCredentials": "保存済みのログイン認証情報はありません",
|
||||||
|
"clearSavedCredential": "保存済み認証情報を解除",
|
||||||
|
"errorLoginCredentialRequired": "保存済みのログイン認証情報を選択してください。",
|
||||||
|
"errorCredentialRequiredFields": "名前とユーザー名は必須です。",
|
||||||
|
"errorCredentialDetails": "ログイン認証情報の詳細取得に失敗しました。",
|
||||||
|
"confirmDeleteCredential": "ログイン認証情報 {name} を削除しますか?",
|
||||||
|
"errorDeleteCredential": "ログイン認証情報の削除に失敗しました。",
|
||||||
"errorBatchAddResult": "一括追加: {successCount} 件成功, {errorCount} 件失敗。最初のエラー: {firstErrorEncountered}",
|
"errorBatchAddResult": "一括追加: {successCount} 件成功, {errorCount} 件失敗。最初のエラー: {firstErrorEncountered}",
|
||||||
"successBatchAddResult": "一括追加成功: {successCount} 件の接続が作成されました。",
|
"successBatchAddResult": "一括追加成功: {successCount} 件の接続が作成されました。",
|
||||||
"errorIpRangeNotAllowedInEditMode": "編集モードではIP範囲はサポートされていません。単一のIPアドレスを使用してください。",
|
"errorIpRangeNotAllowedInEditMode": "編集モードではIP範囲はサポートされていません。単一のIPアドレスを使用してください。",
|
||||||
|
|||||||
@@ -217,6 +217,8 @@
|
|||||||
"title": "批量编辑连接",
|
"title": "批量编辑连接",
|
||||||
"selectedItems": "已选项目",
|
"selectedItems": "已选项目",
|
||||||
"noChange": "保持不变",
|
"noChange": "保持不变",
|
||||||
|
"savedCredentialMixedType": "批量应用已保存凭证前,请先只选择同一种连接类型。",
|
||||||
|
"savedCredentialTypeHint": "仅支持同类型连接批量应用",
|
||||||
"deleteSelectedButton": "删除选中",
|
"deleteSelectedButton": "删除选中",
|
||||||
"deleteSelectedTooltip": "删除选中的连接",
|
"deleteSelectedTooltip": "删除选中的连接",
|
||||||
"confirmMessage": "您确定要删除选中的 {count} 个连接吗?此操作无法撤销。",
|
"confirmMessage": "您确定要删除选中的 {count} 个连接吗?此操作无法撤销。",
|
||||||
@@ -273,6 +275,23 @@
|
|||||||
"connectionModeProxy": "代理服务器",
|
"connectionModeProxy": "代理服务器",
|
||||||
"connectionModeJumpHost": "跳板机",
|
"connectionModeJumpHost": "跳板机",
|
||||||
"connectionType": "连接类型",
|
"connectionType": "连接类型",
|
||||||
|
"credentialSource": "认证来源",
|
||||||
|
"credentialSourceDirect": "账号密码 / 密钥",
|
||||||
|
"credentialSourceSaved": "使用已保存凭证",
|
||||||
|
"savedLoginCredential": "登录凭证",
|
||||||
|
"selectLoginCredential": "请选择登录凭证",
|
||||||
|
"manageLoginCredentials": "管理登录凭证",
|
||||||
|
"savedCredentialHint": "已保存凭证会在连接和测试时优先使用;切回直填后仍可继续手工输入。",
|
||||||
|
"loginCredentialManager": "登录凭证",
|
||||||
|
"addLoginCredential": "新增登录凭证",
|
||||||
|
"editLoginCredential": "编辑登录凭证",
|
||||||
|
"noLoginCredentials": "暂无登录凭证",
|
||||||
|
"clearSavedCredential": "取消已保存凭证",
|
||||||
|
"errorLoginCredentialRequired": "请选择已保存的登录凭证。",
|
||||||
|
"errorCredentialRequiredFields": "名称和用户名为必填项。",
|
||||||
|
"errorCredentialDetails": "获取登录凭证详情失败。",
|
||||||
|
"confirmDeleteCredential": "确认删除登录凭证 {name}?",
|
||||||
|
"errorDeleteCredential": "删除登录凭证失败。",
|
||||||
"typeSsh": "SSH",
|
"typeSsh": "SSH",
|
||||||
"typeRdp": "RDP",
|
"typeRdp": "RDP",
|
||||||
"typeVnc": "VNC",
|
"typeVnc": "VNC",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface ConnectionInfo {
|
|||||||
port: number;
|
port: number;
|
||||||
username: string;
|
username: string;
|
||||||
auth_method: 'password' | 'key';
|
auth_method: 'password' | 'key';
|
||||||
|
login_credential_id?: number | null;
|
||||||
proxy_id?: number | null; // 关联的代理 ID (可选)
|
proxy_id?: number | null; // 关联的代理 ID (可选)
|
||||||
proxy_type?: 'proxy' | 'jump' | null;
|
proxy_type?: 'proxy' | 'jump' | null;
|
||||||
tag_ids?: number[]; // 关联的标签 ID 数组 (可选)
|
tag_ids?: number[]; // 关联的标签 ID 数组 (可选)
|
||||||
@@ -95,6 +96,7 @@ export const useConnectionsStore = defineStore('connections', {
|
|||||||
port: number;
|
port: number;
|
||||||
username: string;
|
username: string;
|
||||||
auth_method: 'password' | 'key'; // SSH specific
|
auth_method: 'password' | 'key'; // SSH specific
|
||||||
|
login_credential_id?: number | null;
|
||||||
password?: string; // SSH password or general password
|
password?: string; // SSH password or general password
|
||||||
private_key?: string; // SSH specific
|
private_key?: string; // SSH specific
|
||||||
passphrase?: string; // SSH specific
|
passphrase?: string; // SSH specific
|
||||||
@@ -129,7 +131,7 @@ export const useConnectionsStore = defineStore('connections', {
|
|||||||
// 更新连接 Action
|
// 更新连接 Action
|
||||||
// 更新参数类型以包含 proxy_id 和 tag_ids
|
// 更新参数类型以包含 proxy_id 和 tag_ids
|
||||||
// Update parameter type to include 'type' and VNC fields
|
// Update parameter type to include 'type' and VNC fields
|
||||||
async updateConnection(connectionId: number, updatedData: Partial<Omit<ConnectionInfo, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { type?: 'SSH' | 'RDP' | 'VNC'; password?: string; private_key?: string; passphrase?: string; vncPassword?: string; proxy_id?: number | null; proxy_type?: 'proxy' | 'jump' | null; tag_ids?: number[]; jump_chain?: number[] | null; }>) {
|
async updateConnection(connectionId: number, updatedData: Partial<Omit<ConnectionInfo, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { type?: 'SSH' | 'RDP' | 'VNC'; password?: string; private_key?: string; passphrase?: string; vncPassword?: string; proxy_id?: number | null; proxy_type?: 'proxy' | 'jump' | null; tag_ids?: number[]; jump_chain?: number[] | null; login_credential_id?: number | null; }>) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import AddConnectionForm from '../components/AddConnectionForm.vue';
|
import AddConnectionForm from '../components/AddConnectionForm.vue';
|
||||||
import BatchEditConnectionForm from '../components/BatchEditConnectionForm.vue';
|
import BatchEditConnectionForm from '../components/BatchEditConnectionForm.vue';
|
||||||
|
import LoginCredentialManagementModal from '../components/LoginCredentialManagementModal.vue';
|
||||||
import { useConnectionsStore } from '../stores/connections.store';
|
import { useConnectionsStore } from '../stores/connections.store';
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useTagsStore } from '../stores/tags.store';
|
import { useTagsStore } from '../stores/tags.store';
|
||||||
@@ -89,6 +90,7 @@ const tagsSectionExpanded = ref(true);
|
|||||||
|
|
||||||
const showAddEditConnectionForm = ref(false);
|
const showAddEditConnectionForm = ref(false);
|
||||||
const connectionToEdit = ref<ConnectionInfo | null>(null);
|
const connectionToEdit = ref<ConnectionInfo | null>(null);
|
||||||
|
const showLoginCredentialManagement = ref(false);
|
||||||
|
|
||||||
const isBatchEditMode = ref(false);
|
const isBatchEditMode = ref(false);
|
||||||
const selectedConnectionIdsForBatch = ref<Set<number>>(new Set());
|
const selectedConnectionIdsForBatch = ref<Set<number>>(new Set());
|
||||||
@@ -1287,6 +1289,14 @@ onBeforeUnmount(() => {
|
|||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span>{{ t('connections.addConnection', '新增连接') }}</span>
|
<span>{{ t('connections.addConnection', '新增连接') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click="showLoginCredentialManagement = true"
|
||||||
|
class="h-11 px-4 rounded-xl border border-border bg-background text-foreground hover:bg-border transition-colors inline-flex items-center gap-2"
|
||||||
|
:title="t('connections.form.loginCredentialManager', '登录凭证')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
<span>{{ t('connections.form.loginCredentialManager', '登录凭证') }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="handleTestAllFilteredConnections"
|
@click="handleTestAllFilteredConnections"
|
||||||
:disabled="isTestingAll || isLoadingConnections || !filteredAndSortedConnections.some(c => c.type === 'SSH')"
|
:disabled="isTestingAll || isLoadingConnections || !filteredAndSortedConnections.some(c => c.type === 'SSH')"
|
||||||
@@ -1620,6 +1630,11 @@ onBeforeUnmount(() => {
|
|||||||
@update:visible="handleBatchEditFormClose"
|
@update:visible="handleBatchEditFormClose"
|
||||||
@saved="handleBatchEditSaved"
|
@saved="handleBatchEditSaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LoginCredentialManagementModal
|
||||||
|
v-if="showLoginCredentialManagement"
|
||||||
|
@close="showLoginCredentialManagement = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user