fix: use native login inputs

This commit is contained in:
jxxghp
2026-06-30 06:51:57 +08:00
parent 2d8ae9f462
commit 878c3c98b2
4 changed files with 136 additions and 80 deletions
+2
View File
@@ -285,6 +285,8 @@ export default {
copyright: '© {year} MoviePilot', copyright: '© {year} MoviePilot',
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
showPassword: 'Show password',
hidePassword: 'Hide password',
otpCode: 'Verification Code', otpCode: 'Verification Code',
stayLoggedIn: 'Stay Logged In', stayLoggedIn: 'Stay Logged In',
login: 'Login', login: 'Login',
+2
View File
@@ -282,6 +282,8 @@ export default {
copyright: '© {year} MoviePilot', copyright: '© {year} MoviePilot',
username: '用户名', username: '用户名',
password: '密码', password: '密码',
showPassword: '显示密码',
hidePassword: '隐藏密码',
otpCode: '验证码', otpCode: '验证码',
stayLoggedIn: '保持登录', stayLoggedIn: '保持登录',
login: '登录', login: '登录',
+2
View File
@@ -282,6 +282,8 @@ export default {
copyright: '© {year} MoviePilot', copyright: '© {year} MoviePilot',
username: '用戶名', username: '用戶名',
password: '密碼', password: '密碼',
showPassword: '顯示密碼',
hidePassword: '隱藏密碼',
otpCode: '驗證碼', otpCode: '驗證碼',
stayLoggedIn: '保持登錄', stayLoggedIn: '保持登錄',
login: '登錄', login: '登錄',
+130 -80
View File
@@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Component } from 'vue' import type { Component } from 'vue'
import { VForm } from 'vuetify/components/VForm'
import { useAuthStore, useUserStore } from '@/stores' import { useAuthStore, useUserStore } from '@/stores'
import { authState, userState } from '@/stores/types' import { authState, userState } from '@/stores/types'
import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import router from '@/router' import router from '@/router'
import logo from '@images/logo.png' import logo from '@images/logo.png'
@@ -43,7 +41,7 @@ const form = ref({
remember: true, remember: true,
}) })
const refForm = ref<InstanceType<typeof VForm> | null>(null) const refForm = ref<HTMLFormElement | null>(null)
// 密码输入 // 密码输入
const isPasswordVisible = ref(false) const isPasswordVisible = ref(false)
@@ -61,12 +59,6 @@ const mfaDialog = ref(false)
const mfaPasskeyLoading = ref(false) const mfaPasskeyLoading = ref(false)
let mfaDialogController: ReturnType<typeof openSharedDialog> | null = null let mfaDialogController: ReturnType<typeof openSharedDialog> | null = null
// 用户名称输入框
const usernameInput = ref()
// 登录自动填充同步的延时任务
const autofillSyncTimerIds: number[] = []
// 语言选择菜单 // 语言选择菜单
const langMenu = ref(false) const langMenu = ref(false)
@@ -133,8 +125,7 @@ const showPasskeyLogin = computed(() => !!systemPasskeyProvider.value?.enabled)
// 获取登录表单中的原生账号和密码输入框。 // 获取登录表单中的原生账号和密码输入框。
function getLoginCredentialInputs() { function getLoginCredentialInputs() {
const formElement = refForm.value?.$el as HTMLFormElement | undefined const root = refForm.value || document
const root = formElement || document
return { return {
username: root.querySelector<HTMLInputElement>('input[name="username"]'), username: root.querySelector<HTMLInputElement>('input[name="username"]'),
@@ -155,21 +146,6 @@ function syncLoginCredentialValues() {
} }
} }
// 清理尚未执行的登录自动填充同步任务。
function clearLoginAutofillSyncTimers() {
autofillSyncTimerIds.forEach(timerId => window.clearTimeout(timerId))
autofillSyncTimerIds.length = 0
}
// 延迟多次同步自动填充值,兼容浏览器或密码管理器不派发事件的自动填充。
function scheduleLoginAutofillSync() {
clearLoginAutofillSyncTimers()
syncLoginCredentialValues()
autofillSyncTimerIds.push(window.setTimeout(syncLoginCredentialValues, 50))
autofillSyncTimerIds.push(window.setTimeout(syncLoginCredentialValues, 250))
autofillSyncTimerIds.push(window.setTimeout(syncLoginCredentialValues, 1000))
}
// 生成 MFA 共享弹窗使用的最新 props。 // 生成 MFA 共享弹窗使用的最新 props。
function getMfaDialogProps() { function getMfaDialogProps() {
return { return {
@@ -671,10 +647,6 @@ onMounted(async () => {
// 加载系统和插件声明的未登录认证入口 // 加载系统和插件声明的未登录认证入口
await loadAuthProviders() await loadAuthProviders()
// 捕获页面加载后由浏览器或密码管理器写入的静默自动填充值。
await nextTick()
scheduleLoginAutofillSync()
// 初始化 Conditional UI 的 PassKey 自动填充 // 初始化 Conditional UI 的 PassKey 自动填充
await initConditionalPasskey() await initConditionalPasskey()
}) })
@@ -718,7 +690,6 @@ onUnmounted(() => {
manualAbortController.abort() manualAbortController.abort()
manualAbortController = null manualAbortController = null
} }
clearLoginAutofillSyncTimers()
}) })
</script> </script>
@@ -777,48 +748,59 @@ onUnmounted(() => {
</div> </div>
<VCardText class="login-body"> <VCardText class="login-body">
<VForm ref="refForm" autocomplete="on" @submit.prevent="login"> <form
ref="refForm"
class="login-form"
method="post"
action="/login/access-token"
autocomplete="on"
@submit.prevent="login"
>
<VRow> <VRow>
<!-- username --> <!-- username -->
<VCol cols="12"> <VCol cols="12">
<VTextField <div class="native-login-field login-input">
ref="usernameInput" <VIcon icon="mdi-account-outline" class="native-login-field__icon" aria-hidden="true" />
v-model="form.username" <input
:label="t('login.username')" id="username"
type="text" v-model="form.username"
name="username" class="native-login-field__input"
id="username" type="text"
autocomplete="username" name="username"
:rules="[requiredValidator]" autocomplete="username"
hide-details autocapitalize="none"
variant="outlined" spellcheck="false"
density="comfortable" enterkeyhint="next"
prepend-inner-icon="mdi-account-outline" :placeholder="t('login.username')"
class="login-input" :aria-label="t('login.username')"
@input="scheduleLoginAutofillSync" required
@change="scheduleLoginAutofillSync" />
/> </div>
</VCol> </VCol>
<!-- password --> <!-- password -->
<VCol cols="12"> <VCol cols="12">
<VTextField <div class="native-login-field native-login-field--password login-input">
v-model="form.password" <VIcon icon="mdi-lock-outline" class="native-login-field__icon" aria-hidden="true" />
:label="t('login.password')" <input
:type="isPasswordVisible ? 'text' : 'password'" id="password"
name="password" v-model="form.password"
id="password" class="native-login-field__input"
autocomplete="current-password" :type="isPasswordVisible ? 'text' : 'password'"
prepend-inner-icon="mdi-lock-outline" name="password"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" autocomplete="current-password"
:rules="[requiredValidator]" :placeholder="t('login.password')"
hide-details :aria-label="t('login.password')"
variant="outlined" required
density="comfortable" />
class="login-input" <button
@click:append-inner="isPasswordVisible = !isPasswordVisible" class="native-login-field__toggle"
@input="scheduleLoginAutofillSync" type="button"
@change="scheduleLoginAutofillSync" :aria-label="isPasswordVisible ? t('login.hidePassword') : t('login.showPassword')"
/> @click="isPasswordVisible = !isPasswordVisible"
>
<VIcon :icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" size="20" />
</button>
</div>
</VCol> </VCol>
<VCol cols="12" class="py-0"> <VCol cols="12" class="py-0">
<!-- remember me checkbox --> <!-- remember me checkbox -->
@@ -876,7 +858,7 @@ onUnmounted(() => {
</VAlert> </VAlert>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </form>
</VCardText> </VCardText>
<!-- 卡片页脚版权 + 版本 --> <!-- 卡片页脚版权 + 版本 -->
@@ -1167,24 +1149,92 @@ onUnmounted(() => {
padding-block: 8px !important; padding-block: 8px !important;
} }
/* 输入框增强样式 */ /* 原生登录输入框:保留标准 input DOM,便于密码管理器识别。 */
:deep(.login-input .v-field) { .native-login-field {
position: relative;
display: flex;
align-items: center;
border: 1px solid rgba(var(--v-border-color), 0.38);
min-block-size: 56px;
border-radius: 12px; border-radius: 12px;
background: transparent;
transition: transition:
box-shadow 200ms ease, border-color 150ms ease,
border-color 200ms ease; box-shadow 150ms ease;
} }
/* 输入框聚焦时增加主色光晕 */ .native-login-field:focus-within {
:deep(.login-body .v-field.v-field--focused) { border-color: rgb(var(--v-theme-primary));
box-shadow: box-shadow: inset 0 0 0 1px rgb(var(--v-theme-primary));
0 0 0 3px rgba(var(--v-theme-primary), 0.12),
0 4px 12px rgba(var(--v-theme-primary), 0.08);
} }
/* 输入框悬停 */ .native-login-field__icon {
:deep(.login-input .v-field:hover:not(.v-field--focused)) { position: absolute;
border-color: rgba(var(--v-theme-primary), 0.4); z-index: 1;
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
inset-block-start: 16px;
inset-inline-start: 16px;
pointer-events: none;
}
.native-login-field__input {
display: block;
border: 0;
appearance: none;
background: transparent;
block-size: 54px;
color: rgb(var(--v-theme-on-surface));
font: inherit;
inline-size: 100%;
line-height: 1.5;
outline: none;
padding-block: 0;
padding-inline: 48px 16px;
}
.native-login-field__input::placeholder {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
opacity: 1;
}
.native-login-field--password .native-login-field__input {
padding-inline-end: 48px;
}
.native-login-field__input:-webkit-autofill,
.native-login-field__input:-webkit-autofill:hover,
.native-login-field__input:-webkit-autofill:focus {
-webkit-text-fill-color: rgb(var(--v-theme-on-surface));
caret-color: rgb(var(--v-theme-on-surface));
transition: background-color 9999s ease-in-out 0s;
}
.native-login-field__toggle {
position: absolute;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 50%;
background: transparent;
block-size: 40px;
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
cursor: pointer;
inline-size: 40px;
inset-block-start: 8px;
inset-inline-end: 8px;
padding: 0;
transition:
background 150ms ease,
color 150ms ease;
}
.native-login-field__toggle:hover,
.native-login-field__toggle:focus-visible {
background: rgba(var(--v-theme-on-surface), 0.08);
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
outline: none;
} }
/* Remember me 复选框样式优化 */ /* Remember me 复选框样式优化 */