feat(ui): add password visibility toggle to all password inputs

This commit is contained in:
t8y2
2026-06-15 02:34:19 +08:00
parent 29edc7b7b7
commit 8af25343cc
6 changed files with 57 additions and 26 deletions
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
@@ -502,7 +503,7 @@ onMounted(loadUsers);
{{ t("userAdmin.allowLogin") }}
</label>
<label class="block text-xs font-medium">{{ t("connection.password") }}</label>
<Input v-model="createPassword" type="password" />
<PasswordInput v-model="createPassword" />
</div>
<DialogFooter>
<Button variant="outline" @click="createDialogOpen = false">{{ t("dangerDialog.cancel") }}</Button>
@@ -518,7 +519,7 @@ onMounted(loadUsers);
<DialogHeader>
<DialogTitle>{{ t("userAdmin.changePassword") }}</DialogTitle>
</DialogHeader>
<Input v-model="newPassword" type="password" :placeholder="t('userAdmin.newPassword')" />
<PasswordInput v-model="newPassword" :placeholder="t('userAdmin.newPassword')" />
<DialogFooter>
<Button variant="outline" @click="passwordDialogOpen = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="!newPassword" @click="previewPasswordChange">{{ t("userAdmin.previewSql") }}</Button>
@@ -3,6 +3,7 @@ import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Lock, Loader2, ShieldCheck } from "@lucide/vue";
import AppLogo from "@/components/icons/AppLogo.vue";
@@ -70,11 +71,11 @@ async function submit() {
</div>
<div class="relative">
<Lock class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input v-model="password" type="password" :placeholder="setupMode ? t('auth.newPassword') : t('auth.enterPassword')" class="pl-10 h-11" autocomplete="off" autofocus />
<PasswordInput v-model="password" :placeholder="setupMode ? t('auth.newPassword') : t('auth.enterPassword')" inputClass="pl-10 h-11" autocomplete="off" autofocus />
</div>
<div v-if="setupMode" class="relative">
<Lock class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input v-model="confirmPassword" type="password" :placeholder="t('auth.confirmPassword')" class="pl-10 h-11" autocomplete="off" />
<PasswordInput v-model="confirmPassword" :placeholder="t('auth.confirmPassword')" inputClass="pl-10 h-11" autocomplete="off" />
</div>
<p v-if="error" class="text-sm text-destructive text-center">{{ error }}</p>
<Button type="submit" class="w-full h-11 text-sm font-medium" :disabled="loading || !password || (setupMode && !confirmPassword)">
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
import { Lock } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Label } from "@/components/ui/label";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -76,12 +77,12 @@ const displayError = computed(() => error.value || props.externalError || "");
<div class="grid gap-2">
<Label>{{ t("configExport.passphrase") }}</Label>
<Input v-model="passphrase" type="password" :placeholder="t('configExport.passphrasePlaceholder')" @keydown.enter="mode === 'import' ? confirm() : undefined" />
<PasswordInput v-model="passphrase" :placeholder="t('configExport.passphrasePlaceholder')" @keydown.enter="mode === 'import' ? confirm() : undefined" />
</div>
<div v-if="mode === 'export'" class="grid gap-2">
<Label>{{ t("configExport.passphraseConfirm") }}</Label>
<Input v-model="passphraseConfirm" type="password" :placeholder="t('configExport.passphraseConfirmPlaceholder')" @keydown.enter="confirm" />
<PasswordInput v-model="passphraseConfirm" :placeholder="t('configExport.passphraseConfirmPlaceholder')" @keydown.enter="confirm" />
</div>
<p v-if="displayError" class="text-sm text-destructive">{{ displayError }}</p>
@@ -6,6 +6,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -2257,7 +2258,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-start gap-4">
<Label class="text-right mt-2">{{ t("connection.jdbcDriverPaths") }}</Label>
@@ -2378,7 +2379,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}{{ form.db_type === "access" ? "(可选)" : "" }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
</template>
</template>
@@ -2424,7 +2425,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.redisSentinelPassword") }}</Label>
<Input v-model="form.redis_sentinel_password" type="password" class="col-span-3" />
<PasswordInput v-model="form.redis_sentinel_password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.redisSentinelTls") }}</Label>
@@ -2451,7 +2452,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" :placeholder="t('connection.databasePlaceholder')" />
<PasswordInput v-model="form.password" class="col-span-3" :placeholder="t('connection.databasePlaceholder')" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.redisKeySeparator") }}</Label>
@@ -2486,7 +2487,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
</template>
@@ -2528,7 +2529,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.defaultDatabase") }}</Label>
@@ -2578,7 +2579,7 @@ function openExternalUrl(url: string) {
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">Auth Token</Label>
<Input v-model="form.password" type="password" class="col-span-3" placeholder="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..." />
<PasswordInput v-model="form.password" class="col-span-3" placeholder="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..." />
</div>
<div class="grid grid-cols-4 items-start gap-4">
@@ -2612,7 +2613,7 @@ function openExternalUrl(url: string) {
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
@@ -3061,7 +3062,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.sshPassword") }}</Label>
<Input v-model="selectedSshLayer.password" type="password" class="col-span-3" :placeholder="t('connection.sshPasswordPlaceholder')" :disabled="selectedSshLayer.enabled === false" />
<PasswordInput v-model="selectedSshLayer.password" class="col-span-3" :placeholder="t('connection.sshPasswordPlaceholder')" :disabled="selectedSshLayer.enabled === false" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.sshKeyPath") }}</Label>
@@ -3079,7 +3080,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.sshKeyPassphrase") }}</Label>
<Input v-model="selectedSshLayer.key_passphrase" type="password" class="col-span-3" :placeholder="t('connection.sshKeyPassphrasePlaceholder')" :disabled="selectedSshLayer.enabled === false" />
<PasswordInput v-model="selectedSshLayer.key_passphrase" class="col-span-3" :placeholder="t('connection.sshKeyPassphrasePlaceholder')" :disabled="selectedSshLayer.enabled === false" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<span />
@@ -3124,7 +3125,7 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.proxyPassword") }}</Label>
<Input v-model="selectedProxyLayer.password" type="password" class="col-span-3" :placeholder="t('connection.proxyPasswordPlaceholder')" :disabled="selectedProxyLayer.enabled === false" />
<PasswordInput v-model="selectedProxyLayer.password" class="col-span-3" :placeholder="t('connection.proxyPasswordPlaceholder')" :disabled="selectedProxyLayer.enabled === false" />
</div>
</template>
</template>
@@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Label } from "@/components/ui/label";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -2055,7 +2056,7 @@ watch(
<div class="space-y-2">
<Label for="webdav-password">{{ t("settings.syncPassword") }}</Label>
<div class="relative">
<Input id="webdav-password" v-model="webdavPassword" type="password" :placeholder="webdavHasSavedPassword ? '••••••••' : t('settings.syncPasswordPlaceholder')" :disabled="webdavHasSavedPassword" autocomplete="current-password" />
<PasswordInput id="webdav-password" v-model="webdavPassword" :placeholder="webdavHasSavedPassword ? '••••••••' : t('settings.syncPasswordPlaceholder')" :disabled="webdavHasSavedPassword" autocomplete="current-password" />
<Button
v-if="webdavHasSavedPassword"
variant="ghost"
@@ -2109,7 +2110,7 @@ watch(
</div>
<div v-if="webdavSyncSecrets" class="space-y-2">
<Label for="webdav-secrets-passphrase">{{ t("settings.syncSecretsPassphrase") }}</Label>
<Input id="webdav-secrets-passphrase" v-model="webdavSecretsPassphrase" type="password" autocomplete="new-password" />
<PasswordInput id="webdav-secrets-passphrase" v-model="webdavSecretsPassphrase" autocomplete="new-password" />
<p class="text-xs text-muted-foreground">{{ t("settings.syncSecretsPassphraseDescription") }}</p>
</div>
</div>
@@ -2121,7 +2122,7 @@ watch(
<div class="grid grid-cols-3 items-center gap-3">
<Label class="text-right text-xs">{{ t("ai.provider") }}</Label>
<Select :model-value="aiEditProvider" @update:model-value="(v: any) => aiSelectProvider(v)">
<SelectTrigger class="col-span-2 h-8 text-xs">
<SelectTrigger class="col-span-2" inputClass="h-8 text-xs">
<SelectValue>
<span class="flex items-center gap-2">
<AiProviderLogo :provider="selectedAiProviderPreset.provider" :label="selectedAiProviderPreset.label" :icon-slug="selectedAiProviderPreset.iconSlug" />
@@ -2143,7 +2144,7 @@ watch(
<div v-if="aiSupportsAuthMethod" class="grid grid-cols-3 items-center gap-3">
<Label class="text-right text-xs">Authentication</Label>
<Select v-model="aiEditAuthMethod">
<SelectTrigger class="col-span-2 h-8 text-xs">
<SelectTrigger class="col-span-2" inputClass="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -2155,7 +2156,7 @@ watch(
<div class="grid grid-cols-3 items-center gap-3">
<Label class="text-right text-xs">{{ aiCredentialLabel }}</Label>
<Input v-model="aiEditApiKey" type="password" autocomplete="off" class="col-span-2 h-8 text-xs" :placeholder="aiCredentialPlaceholder" />
<PasswordInput v-model="aiEditApiKey" autocomplete="off" class="col-span-2" inputClass="h-8 text-xs" :placeholder="aiCredentialPlaceholder" />
</div>
<div class="grid grid-cols-3 items-start gap-3">
@@ -2243,7 +2244,7 @@ watch(
<div class="grid grid-cols-3 items-center gap-3">
<Label class="text-right text-xs">{{ t("ai.proxyUrl") }}</Label>
<Input v-model="aiEditProxyUrl" autocomplete="off" class="col-span-2 h-8 text-xs" placeholder="socks5://127.0.0.1:7890" :disabled="!aiEditProxyEnabled" />
<Input v-model="aiEditProxyUrl" autocomplete="off" class="col-span-2" inputClass="h-8 text-xs" placeholder="socks5://127.0.0.1:7890" :disabled="!aiEditProxyEnabled" />
</div>
</div>
</section>
@@ -2401,9 +2402,9 @@ watch(
<div class="space-y-3">
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
<p class="text-sm text-muted-foreground">{{ t("auth.changePasswordDescription") }}</p>
<Input v-model="oldPassword" type="password" :placeholder="t('auth.oldPassword')" class="h-9" autocomplete="off" />
<Input v-model="newPassword" type="password" :placeholder="t('auth.newPassword')" class="h-9" autocomplete="off" />
<Input v-model="confirmNewPassword" type="password" :placeholder="t('auth.confirmPassword')" class="h-9" autocomplete="off" />
<PasswordInput v-model="oldPassword" :placeholder="t('auth.oldPassword')" inputClass="h-9" autocomplete="off" />
<PasswordInput v-model="newPassword" :placeholder="t('auth.newPassword')" inputClass="h-9" autocomplete="off" />
<PasswordInput v-model="confirmNewPassword" :placeholder="t('auth.confirmPassword')" inputClass="h-9" autocomplete="off" />
<p v-if="passwordMessage" class="text-xs" :class="passwordError ? 'text-destructive' : 'text-green-500'">
{{ passwordMessage }}
</p>
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { ref } from "vue";
import { Eye, EyeOff } from "@lucide/vue";
import { Input } from "@/components/ui/input";
const model = defineModel<string>();
const props = defineProps<{
placeholder?: string;
disabled?: boolean;
class?: string;
inputClass?: string;
}>();
const visible = ref(false);
</script>
<template>
<div :class="props.class" class="relative">
<Input v-model="model" :type="visible ? 'text' : 'password'" :placeholder="placeholder" :disabled="disabled" :class="props.inputClass" class="pr-8" v-bind="$attrs" />
<button type="button" :disabled="disabled" class="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground disabled:pointer-events-none" tabindex="-1" @click="visible = !visible">
<Eye v-if="!visible" class="size-3.5" />
<EyeOff v-else class="size-3.5" />
</button>
</div>
</template>