重构(前端): 持久化快速命令排序和密码切换
添加持久化排序字段并重新排序快速命令和标签的端点,更新前端以支持手动拖放排序,并为连接和凭据表单添加密码可见性切换。此外,将 SSH 连接测试作为连接列表中的默认操作,并刷新相关模块文档和更改日志。
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SshKeySelector from './SshKeySelector.vue'; // Assuming SshKeySelector is used here
|
||||
import LoginCredentialSelector from './LoginCredentialSelector.vue';
|
||||
@@ -19,6 +20,26 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visiblePasswordFields = reactive({
|
||||
sshPassword: false,
|
||||
rdpPassword: false,
|
||||
vncPassword: false,
|
||||
});
|
||||
|
||||
const resetPasswordVisibility = (): void => {
|
||||
visiblePasswordFields.sshPassword = false;
|
||||
visiblePasswordFields.rdpPassword = false;
|
||||
visiblePasswordFields.vncPassword = false;
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (field: keyof typeof visiblePasswordFields): void => {
|
||||
visiblePasswordFields[field] = !visiblePasswordFields[field];
|
||||
};
|
||||
|
||||
watch(() => props.formData.type, resetPasswordVisibility);
|
||||
watch(() => props.formData.auth_method, resetPasswordVisibility);
|
||||
watch(() => props.formData.credential_source, resetPasswordVisibility);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,8 +107,20 @@ const { t } = useI18n();
|
||||
|
||||
<div v-if="props.formData.auth_method === 'password'">
|
||||
<label for="conn-password" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password') }}</label>
|
||||
<input type="password" id="conn-password" v-model="props.formData.password" :required="props.formData.auth_method === 'password' && !isEditMode" autocomplete="new-password"
|
||||
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" />
|
||||
<div class="relative">
|
||||
<input :type="visiblePasswordFields.sshPassword ? 'text' : 'password'" id="conn-password" v-model="props.formData.password" :required="props.formData.auth_method === 'password' && !isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.sshPassword"
|
||||
@click="togglePasswordVisibility('sshPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.sshPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.formData.auth_method === 'key'" class="space-y-4">
|
||||
@@ -102,8 +135,20 @@ const { t } = useI18n();
|
||||
<template v-if="props.formData.type === 'RDP'">
|
||||
<div>
|
||||
<label for="conn-password-rdp" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password') }}</label>
|
||||
<input type="password" id="conn-password-rdp" v-model="props.formData.password" :required="!isEditMode" autocomplete="new-password"
|
||||
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" />
|
||||
<div class="relative">
|
||||
<input :type="visiblePasswordFields.rdpPassword ? 'text' : 'password'" id="conn-password-rdp" v-model="props.formData.password" :required="!isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.rdpPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.rdpPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.rdpPassword"
|
||||
@click="togglePasswordVisibility('rdpPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.rdpPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,8 +156,20 @@ const { t } = useI18n();
|
||||
<template v-if="props.formData.type === 'VNC'">
|
||||
<div>
|
||||
<label for="conn-password-vnc" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.vncPassword', 'VNC 密码') }}</label>
|
||||
<input type="password" id="conn-password-vnc" v-model="props.formData.vncPassword" :required="!isEditMode" autocomplete="new-password"
|
||||
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" />
|
||||
<div class="relative">
|
||||
<input :type="visiblePasswordFields.vncPassword ? 'text' : 'password'" id="conn-password-vnc" v-model="props.formData.vncPassword" :required="!isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.vncPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.vncPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.vncPassword"
|
||||
@click="togglePasswordVisibility('vncPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.vncPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
ref="modalContentRef"
|
||||
class="bg-background text-foreground p-6 rounded-xl border border-border/50 shadow-2xl flex flex-col"
|
||||
:style="{
|
||||
width: resizableWidth ? `${resizableWidth}px` : undefined,
|
||||
width: resizableWidth ? `${resizableWidth}px` : `min(calc(100vw - ${MODAL_VIEWPORT_GUTTER_PX}px), ${MODAL_DEFAULT_WIDTH_RATIO * 100}vw)`,
|
||||
height: resizableHeight ? `${resizableHeight}px` : undefined,
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
maxWidth: `calc(100vw - ${MODAL_VIEWPORT_GUTTER_PX}px)`,
|
||||
maxHeight: 'calc(100vh - 2rem)',
|
||||
}"
|
||||
>
|
||||
@@ -183,6 +183,8 @@ const modalContentRef = ref<HTMLElement | null>(null);
|
||||
const commandTextareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const R_MIN_WIDTH = 580; // 可调整大小的最小宽度 (像素)
|
||||
const R_MIN_HEIGHT = 440; // 可调整大小的最小高度 (像素)
|
||||
const MODAL_DEFAULT_WIDTH_RATIO = 0.6;
|
||||
const MODAL_VIEWPORT_GUTTER_PX = 32;
|
||||
const placeholder = t('quickCommands.form.commandPlaceholder') + 'echo "Hello,\${USERNAME}"'
|
||||
|
||||
const { width: resizableWidth, height: resizableHeight } = useResizable(modalContentRef, {
|
||||
@@ -239,7 +241,7 @@ watch(() => formData.command, (newCommand) => {
|
||||
// 初始化表单数据 (如果是编辑模式)
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
let initialW = Math.min(window.innerWidth * 0.74, 860); // 目标 74vw,最大 860px
|
||||
let initialW = Math.min(window.innerWidth * MODAL_DEFAULT_WIDTH_RATIO, window.innerWidth - MODAL_VIEWPORT_GUTTER_PX);
|
||||
let initialH = Math.min(window.innerHeight * 0.68, 600); // 目标 68vh,最大 600px
|
||||
|
||||
initialW = Math.max(R_MIN_WIDTH, initialW);
|
||||
|
||||
@@ -39,6 +39,19 @@ const initialFormData: LoginCredentialInput = {
|
||||
|
||||
const formData = reactive({ ...initialFormData });
|
||||
const formError = ref<string | null>(null);
|
||||
const visiblePasswordFields = reactive({
|
||||
sshPassword: false,
|
||||
genericPassword: false,
|
||||
});
|
||||
|
||||
const resetPasswordVisibility = (): void => {
|
||||
visiblePasswordFields.sshPassword = false;
|
||||
visiblePasswordFields.genericPassword = false;
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (field: keyof typeof visiblePasswordFields): void => {
|
||||
visiblePasswordFields[field] = !visiblePasswordFields[field];
|
||||
};
|
||||
|
||||
watch(() => props.initialType, (newValue) => {
|
||||
if (!credentialToEdit.value && newValue) {
|
||||
@@ -47,12 +60,17 @@ watch(() => props.initialType, (newValue) => {
|
||||
});
|
||||
|
||||
watch(() => formData.type, (newType) => {
|
||||
resetPasswordVisibility();
|
||||
if (newType !== 'SSH') {
|
||||
formData.auth_method = 'password';
|
||||
formData.ssh_key_id = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => formData.auth_method, () => {
|
||||
resetPasswordVisibility();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loginCredentialsStore.fetchLoginCredentials();
|
||||
});
|
||||
@@ -60,6 +78,7 @@ onMounted(() => {
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, initialFormData, { type: props.initialType || 'SSH' });
|
||||
formError.value = null;
|
||||
resetPasswordVisibility();
|
||||
};
|
||||
|
||||
const showAddForm = () => {
|
||||
@@ -71,6 +90,7 @@ const showAddForm = () => {
|
||||
const showEditForm = async (credential: LoginCredentialBasicInfo) => {
|
||||
formError.value = null;
|
||||
credentialToEdit.value = credential;
|
||||
resetPasswordVisibility();
|
||||
|
||||
const details = await loginCredentialsStore.fetchLoginCredentialDetails(credential.id);
|
||||
if (!details) {
|
||||
@@ -300,7 +320,19 @@ const cancelForm = () => {
|
||||
</div>
|
||||
<div v-if="formData.auth_method === 'password'">
|
||||
<label for="credential-password" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password', '密码') }}</label>
|
||||
<input id="credential-password" v-model="formData.password" type="password" autocomplete="new-password" 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" />
|
||||
<div class="relative">
|
||||
<input id="credential-password" v-model="formData.password" :type="visiblePasswordFields.sshPassword ? 'text' : 'password'" autocomplete="new-password" class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.sshPassword"
|
||||
@click="togglePasswordVisibility('sshPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.sshPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.sshKey', 'SSH 密钥') }}</label>
|
||||
@@ -313,7 +345,19 @@ const cancelForm = () => {
|
||||
|
||||
<div v-else>
|
||||
<label for="credential-password-generic" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password', '密码') }}</label>
|
||||
<input id="credential-password-generic" v-model="formData.password" type="password" autocomplete="new-password" 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" />
|
||||
<div class="relative">
|
||||
<input id="credential-password-generic" v-model="formData.password" :type="visiblePasswordFields.genericPassword ? 'text' : 'password'" autocomplete="new-password" class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.genericPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.genericPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.genericPassword"
|
||||
@click="togglePasswordVisibility('genericPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.genericPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user